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, PyList};
9#[cfg(feature = "python")]
10use pyo3::wrap_pyfunction;
11
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use serde::{Deserialize, Serialize};
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use wasm_bindgen::prelude::*;
16
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21 make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::convert::AsRef;
28use std::mem::ManuallyDrop;
29use thiserror::Error;
30
31const DEFAULT_LENGTH: usize = 14;
32const DEFAULT_SOURCE: &str = "close";
33const DEFAULT_PRESMOOTH: usize = 10;
34const DEFAULT_POSTSMOOTH: usize = 10;
35const DEFAULT_SMOOTHING_METHOD: &str = "sma";
36const MIN_STOCH_LENGTH: usize = 4;
37const FLOAT_TOL: f64 = 1e-12;
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40enum SmoothingMethod {
41 None,
42 Sma,
43 Tma,
44 Lsma,
45}
46
47impl SmoothingMethod {
48 #[inline]
49 fn parse(name: &str) -> Option<Self> {
50 if name.eq_ignore_ascii_case("none") {
51 Some(Self::None)
52 } else if name.eq_ignore_ascii_case("sma") {
53 Some(Self::Sma)
54 } else if name.eq_ignore_ascii_case("tma") {
55 Some(Self::Tma)
56 } else if name.eq_ignore_ascii_case("lsma") {
57 Some(Self::Lsma)
58 } else {
59 None
60 }
61 }
62
63 #[inline]
64 fn as_str(self) -> &'static str {
65 match self {
66 Self::None => "none",
67 Self::Sma => "sma",
68 Self::Tma => "tma",
69 Self::Lsma => "lsma",
70 }
71 }
72}
73
74impl<'a> AsRef<[f64]> for MultiLengthStochasticAverageInput<'a> {
75 #[inline(always)]
76 fn as_ref(&self) -> &[f64] {
77 match &self.data {
78 MultiLengthStochasticAverageData::Slice(slice) => slice,
79 MultiLengthStochasticAverageData::Candles { candles, source } => {
80 source_type(candles, source)
81 }
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
87pub enum MultiLengthStochasticAverageData<'a> {
88 Candles {
89 candles: &'a Candles,
90 source: &'a str,
91 },
92 Slice(&'a [f64]),
93}
94
95#[derive(Debug, Clone)]
96pub struct MultiLengthStochasticAverageOutput {
97 pub values: Vec<f64>,
98}
99
100#[derive(Debug, Clone, PartialEq)]
101#[cfg_attr(
102 all(target_arch = "wasm32", feature = "wasm"),
103 derive(Serialize, Deserialize)
104)]
105pub struct MultiLengthStochasticAverageParams {
106 pub length: Option<usize>,
107 pub presmooth: Option<usize>,
108 pub premethod: Option<String>,
109 pub postsmooth: Option<usize>,
110 pub postmethod: Option<String>,
111}
112
113impl Default for MultiLengthStochasticAverageParams {
114 fn default() -> Self {
115 Self {
116 length: Some(DEFAULT_LENGTH),
117 presmooth: Some(DEFAULT_PRESMOOTH),
118 premethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
119 postsmooth: Some(DEFAULT_POSTSMOOTH),
120 postmethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
126pub struct MultiLengthStochasticAverageInput<'a> {
127 pub data: MultiLengthStochasticAverageData<'a>,
128 pub params: MultiLengthStochasticAverageParams,
129}
130
131impl<'a> MultiLengthStochasticAverageInput<'a> {
132 #[inline]
133 pub fn from_candles(
134 candles: &'a Candles,
135 source: &'a str,
136 params: MultiLengthStochasticAverageParams,
137 ) -> Self {
138 Self {
139 data: MultiLengthStochasticAverageData::Candles { candles, source },
140 params,
141 }
142 }
143
144 #[inline]
145 pub fn from_slice(slice: &'a [f64], params: MultiLengthStochasticAverageParams) -> Self {
146 Self {
147 data: MultiLengthStochasticAverageData::Slice(slice),
148 params,
149 }
150 }
151
152 #[inline]
153 pub fn with_default_candles(candles: &'a Candles) -> Self {
154 Self::from_candles(
155 candles,
156 DEFAULT_SOURCE,
157 MultiLengthStochasticAverageParams::default(),
158 )
159 }
160}
161
162#[derive(Clone, Debug)]
163pub struct MultiLengthStochasticAverageBuilder {
164 length: Option<usize>,
165 presmooth: Option<usize>,
166 premethod: Option<String>,
167 postsmooth: Option<usize>,
168 postmethod: Option<String>,
169 kernel: Kernel,
170}
171
172impl Default for MultiLengthStochasticAverageBuilder {
173 fn default() -> Self {
174 Self {
175 length: None,
176 presmooth: None,
177 premethod: None,
178 postsmooth: None,
179 postmethod: None,
180 kernel: Kernel::Auto,
181 }
182 }
183}
184
185impl MultiLengthStochasticAverageBuilder {
186 #[inline]
187 pub fn new() -> Self {
188 Self::default()
189 }
190
191 #[inline]
192 pub fn length(mut self, length: usize) -> Self {
193 self.length = Some(length);
194 self
195 }
196
197 #[inline]
198 pub fn presmooth(mut self, presmooth: usize) -> Self {
199 self.presmooth = Some(presmooth);
200 self
201 }
202
203 #[inline]
204 pub fn premethod<T: Into<String>>(mut self, premethod: T) -> Self {
205 self.premethod = Some(premethod.into());
206 self
207 }
208
209 #[inline]
210 pub fn postsmooth(mut self, postsmooth: usize) -> Self {
211 self.postsmooth = Some(postsmooth);
212 self
213 }
214
215 #[inline]
216 pub fn postmethod<T: Into<String>>(mut self, postmethod: T) -> Self {
217 self.postmethod = Some(postmethod.into());
218 self
219 }
220
221 #[inline]
222 pub fn kernel(mut self, kernel: Kernel) -> Self {
223 self.kernel = kernel;
224 self
225 }
226
227 #[inline]
228 pub fn apply(
229 self,
230 candles: &Candles,
231 source: &str,
232 ) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
233 let input = MultiLengthStochasticAverageInput::from_candles(
234 candles,
235 source,
236 MultiLengthStochasticAverageParams {
237 length: self.length,
238 presmooth: self.presmooth,
239 premethod: self.premethod,
240 postsmooth: self.postsmooth,
241 postmethod: self.postmethod,
242 },
243 );
244 multi_length_stochastic_average_with_kernel(&input, self.kernel)
245 }
246
247 #[inline]
248 pub fn apply_slice(
249 self,
250 data: &[f64],
251 ) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
252 let input = MultiLengthStochasticAverageInput::from_slice(
253 data,
254 MultiLengthStochasticAverageParams {
255 length: self.length,
256 presmooth: self.presmooth,
257 premethod: self.premethod,
258 postsmooth: self.postsmooth,
259 postmethod: self.postmethod,
260 },
261 );
262 multi_length_stochastic_average_with_kernel(&input, self.kernel)
263 }
264
265 #[inline]
266 pub fn into_stream(
267 self,
268 ) -> Result<MultiLengthStochasticAverageStream, MultiLengthStochasticAverageError> {
269 MultiLengthStochasticAverageStream::try_new(MultiLengthStochasticAverageParams {
270 length: self.length,
271 presmooth: self.presmooth,
272 premethod: self.premethod,
273 postsmooth: self.postsmooth,
274 postmethod: self.postmethod,
275 })
276 }
277}
278
279#[derive(Debug, Error)]
280pub enum MultiLengthStochasticAverageError {
281 #[error("multi_length_stochastic_average: Input data slice is empty.")]
282 EmptyInputData,
283 #[error("multi_length_stochastic_average: All values are NaN.")]
284 AllValuesNaN,
285 #[error(
286 "multi_length_stochastic_average: Invalid length: length = {length}, data length = {data_len}"
287 )]
288 InvalidLength { length: usize, data_len: usize },
289 #[error("multi_length_stochastic_average: Invalid presmooth: {presmooth}")]
290 InvalidPresmooth { presmooth: usize },
291 #[error("multi_length_stochastic_average: Invalid postsmooth: {postsmooth}")]
292 InvalidPostsmooth { postsmooth: usize },
293 #[error("multi_length_stochastic_average: Invalid premethod: {premethod}")]
294 InvalidPreMethod { premethod: String },
295 #[error("multi_length_stochastic_average: Invalid postmethod: {postmethod}")]
296 InvalidPostMethod { postmethod: String },
297 #[error(
298 "multi_length_stochastic_average: Not enough valid data: needed = {needed}, valid = {valid}"
299 )]
300 NotEnoughValidData { needed: usize, valid: usize },
301 #[error(
302 "multi_length_stochastic_average: Output length mismatch: expected = {expected}, got = {got}"
303 )]
304 OutputLengthMismatch { expected: usize, got: usize },
305 #[error(
306 "multi_length_stochastic_average: Invalid range: start={start}, end={end}, step={step}"
307 )]
308 InvalidRange {
309 start: String,
310 end: String,
311 step: String,
312 },
313 #[error("multi_length_stochastic_average: Invalid kernel for batch: {0:?}")]
314 InvalidKernelForBatch(Kernel),
315}
316
317#[derive(Clone, Copy, Debug)]
318struct ResolvedParams {
319 length: usize,
320 presmooth: usize,
321 premethod: SmoothingMethod,
322 postsmooth: usize,
323 postmethod: SmoothingMethod,
324}
325
326#[inline(always)]
327fn first_valid_value(data: &[f64]) -> usize {
328 let mut i = 0usize;
329 while i < data.len() {
330 if data[i].is_finite() {
331 return i;
332 }
333 i += 1;
334 }
335 data.len()
336}
337
338#[inline(always)]
339fn max_consecutive_valid_values(data: &[f64]) -> usize {
340 let mut best = 0usize;
341 let mut run = 0usize;
342 for &value in data {
343 if value.is_finite() {
344 run += 1;
345 if run > best {
346 best = run;
347 }
348 } else {
349 run = 0;
350 }
351 }
352 best
353}
354
355#[inline(always)]
356fn smoothing_warmup(method: SmoothingMethod, length: usize) -> usize {
357 match method {
358 SmoothingMethod::None => 0,
359 SmoothingMethod::Sma | SmoothingMethod::Lsma => length.saturating_sub(1),
360 SmoothingMethod::Tma => length.saturating_sub(1).saturating_mul(2),
361 }
362}
363
364#[inline(always)]
365fn total_warmup(params: ResolvedParams) -> usize {
366 smoothing_warmup(params.premethod, params.presmooth)
367 + params.length.saturating_sub(1)
368 + smoothing_warmup(params.postmethod, params.postsmooth)
369}
370
371#[inline(always)]
372fn canonical_method_name(name: Option<&str>, default: &str) -> String {
373 name.unwrap_or(default).to_ascii_lowercase()
374}
375
376#[inline]
377fn resolve_params(
378 params: &MultiLengthStochasticAverageParams,
379 data_len: Option<usize>,
380) -> Result<ResolvedParams, MultiLengthStochasticAverageError> {
381 let length = params.length.unwrap_or(DEFAULT_LENGTH);
382 let presmooth = params.presmooth.unwrap_or(DEFAULT_PRESMOOTH);
383 let postsmooth = params.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH);
384 let premethod_name =
385 canonical_method_name(params.premethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
386 let postmethod_name =
387 canonical_method_name(params.postmethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
388 let premethod = SmoothingMethod::parse(&premethod_name).ok_or_else(|| {
389 MultiLengthStochasticAverageError::InvalidPreMethod {
390 premethod: premethod_name.clone(),
391 }
392 })?;
393 let postmethod = SmoothingMethod::parse(&postmethod_name).ok_or_else(|| {
394 MultiLengthStochasticAverageError::InvalidPostMethod {
395 postmethod: postmethod_name.clone(),
396 }
397 })?;
398
399 if length < MIN_STOCH_LENGTH {
400 return Err(MultiLengthStochasticAverageError::InvalidLength {
401 length,
402 data_len: data_len.unwrap_or(0),
403 });
404 }
405 if presmooth == 0 {
406 return Err(MultiLengthStochasticAverageError::InvalidPresmooth { presmooth });
407 }
408 if postsmooth == 0 {
409 return Err(MultiLengthStochasticAverageError::InvalidPostsmooth { postsmooth });
410 }
411 if let Some(data_len) = data_len {
412 if length > data_len {
413 return Err(MultiLengthStochasticAverageError::InvalidLength { length, data_len });
414 }
415 }
416
417 Ok(ResolvedParams {
418 length,
419 presmooth,
420 premethod,
421 postsmooth,
422 postmethod,
423 })
424}
425
426#[derive(Clone, Debug)]
427struct SmaState {
428 ring: Vec<f64>,
429 head: usize,
430 count: usize,
431 sum: f64,
432}
433
434impl SmaState {
435 #[inline]
436 fn new(length: usize) -> Self {
437 Self {
438 ring: vec![0.0; length.max(1)],
439 head: 0,
440 count: 0,
441 sum: 0.0,
442 }
443 }
444
445 #[inline]
446 fn reset(&mut self) {
447 self.head = 0;
448 self.count = 0;
449 self.sum = 0.0;
450 }
451
452 #[inline]
453 fn update(&mut self, value: f64) -> Option<f64> {
454 let len = self.ring.len();
455 if self.count == len {
456 self.sum -= self.ring[self.head];
457 } else {
458 self.count += 1;
459 }
460 self.ring[self.head] = value;
461 self.sum += value;
462 self.head += 1;
463 if self.head == len {
464 self.head = 0;
465 }
466 if self.count == len {
467 Some(self.sum / len as f64)
468 } else {
469 None
470 }
471 }
472}
473
474#[derive(Clone, Debug)]
475struct LsmaState {
476 ring: Vec<f64>,
477 head: usize,
478 count: usize,
479 sum_y: f64,
480 sum_xy: f64,
481 x_sum: f64,
482 denom: f64,
483}
484
485impl LsmaState {
486 #[inline]
487 fn new(length: usize) -> Self {
488 let n = length.max(1);
489 let n_f = n as f64;
490 let x_sum = ((n * (n - 1)) / 2) as f64;
491 let x2_sum = ((n * (n - 1) * (2 * n - 1)) / 6) as f64;
492 Self {
493 ring: vec![0.0; n],
494 head: 0,
495 count: 0,
496 sum_y: 0.0,
497 sum_xy: 0.0,
498 x_sum,
499 denom: n_f * x2_sum - x_sum * x_sum,
500 }
501 }
502
503 #[inline]
504 fn reset(&mut self) {
505 self.head = 0;
506 self.count = 0;
507 self.sum_y = 0.0;
508 self.sum_xy = 0.0;
509 }
510
511 #[inline]
512 fn update(&mut self, value: f64) -> Option<f64> {
513 let n = self.ring.len();
514 if self.count < n {
515 let idx = self.count;
516 self.ring[self.head] = value;
517 self.head += 1;
518 if self.head == n {
519 self.head = 0;
520 }
521 self.count += 1;
522 self.sum_y += value;
523 self.sum_xy += idx as f64 * value;
524 if self.count == n {
525 Some(self.endpoint())
526 } else {
527 None
528 }
529 } else {
530 let old = self.ring[self.head];
531 let old_sum_y = self.sum_y;
532 self.ring[self.head] = value;
533 self.head += 1;
534 if self.head == n {
535 self.head = 0;
536 }
537 self.sum_y = old_sum_y - old + value;
538 self.sum_xy = self.sum_xy - (old_sum_y - old) + (n - 1) as f64 * value;
539 Some(self.endpoint())
540 }
541 }
542
543 #[inline]
544 fn endpoint(&self) -> f64 {
545 let n = self.ring.len() as f64;
546 let slope = (n * self.sum_xy - self.x_sum * self.sum_y) / self.denom;
547 let intercept = (self.sum_y - slope * self.x_sum) / n;
548 intercept + slope * (self.ring.len() - 1) as f64
549 }
550}
551
552#[derive(Clone, Debug)]
553enum SmoothingState {
554 None,
555 Sma(SmaState),
556 Tma { inner: SmaState, outer: SmaState },
557 Lsma(LsmaState),
558}
559
560impl SmoothingState {
561 #[inline]
562 fn new(method: SmoothingMethod, length: usize) -> Self {
563 match method {
564 SmoothingMethod::None => Self::None,
565 SmoothingMethod::Sma => Self::Sma(SmaState::new(length)),
566 SmoothingMethod::Tma => Self::Tma {
567 inner: SmaState::new(length),
568 outer: SmaState::new(length),
569 },
570 SmoothingMethod::Lsma => Self::Lsma(LsmaState::new(length)),
571 }
572 }
573
574 #[inline]
575 fn reset(&mut self) {
576 match self {
577 Self::None => {}
578 Self::Sma(state) => state.reset(),
579 Self::Tma { inner, outer } => {
580 inner.reset();
581 outer.reset();
582 }
583 Self::Lsma(state) => state.reset(),
584 }
585 }
586
587 #[inline]
588 fn update(&mut self, value: f64) -> Option<f64> {
589 match self {
590 Self::None => Some(value),
591 Self::Sma(state) => state.update(value),
592 Self::Tma { inner, outer } => inner.update(value).and_then(|x| outer.update(x)),
593 Self::Lsma(state) => state.update(value),
594 }
595 }
596}
597
598#[derive(Clone, Debug)]
599pub struct MultiLengthStochasticAverageStream {
600 params: ResolvedParams,
601 pre_smoother: SmoothingState,
602 post_smoother: SmoothingState,
603 ring: Vec<f64>,
604 head: usize,
605 count: usize,
606}
607
608impl MultiLengthStochasticAverageStream {
609 #[inline]
610 pub fn try_new(
611 params: MultiLengthStochasticAverageParams,
612 ) -> Result<Self, MultiLengthStochasticAverageError> {
613 let params = resolve_params(¶ms, None)?;
614 Ok(Self::new_resolved(params))
615 }
616
617 #[inline]
618 fn new_resolved(params: ResolvedParams) -> Self {
619 Self {
620 params,
621 pre_smoother: SmoothingState::new(params.premethod, params.presmooth),
622 post_smoother: SmoothingState::new(params.postmethod, params.postsmooth),
623 ring: vec![0.0; params.length],
624 head: 0,
625 count: 0,
626 }
627 }
628
629 #[inline]
630 pub fn reset(&mut self) {
631 self.pre_smoother.reset();
632 self.post_smoother.reset();
633 self.head = 0;
634 self.count = 0;
635 }
636
637 #[inline]
638 pub fn get_warmup_period(&self) -> usize {
639 total_warmup(self.params)
640 }
641
642 #[inline]
643 pub fn update(&mut self, value: f64) -> Option<f64> {
644 if !value.is_finite() {
645 self.reset();
646 return None;
647 }
648
649 let pre = self.pre_smoother.update(value)?;
650 self.push_pre_value(pre);
651 if self.count < self.params.length {
652 return None;
653 }
654
655 let norm = self.current_norm()?;
656 self.post_smoother.update(norm)
657 }
658
659 #[inline]
660 fn push_pre_value(&mut self, value: f64) {
661 self.ring[self.head] = value;
662 self.head += 1;
663 if self.head == self.params.length {
664 self.head = 0;
665 }
666 if self.count < self.params.length {
667 self.count += 1;
668 }
669 }
670
671 #[inline]
672 fn current_norm(&mut self) -> Option<f64> {
673 let len = self.params.length;
674 let newest = (self.head + len - 1) % len;
675 let current = self.ring[newest];
676 let mut min_value = current;
677 let mut max_value = current;
678 let mut idx = newest;
679 let mut sum = 0.0;
680
681 for window in 1..=len {
682 let value = self.ring[idx];
683 if value < min_value {
684 min_value = value;
685 }
686 if value > max_value {
687 max_value = value;
688 }
689 if window >= MIN_STOCH_LENGTH {
690 let denom = max_value - min_value;
691 if denom.abs() <= FLOAT_TOL {
692 self.post_smoother.reset();
693 return None;
694 }
695 sum += (current - min_value) / denom;
696 }
697 idx = if idx == 0 { len - 1 } else { idx - 1 };
698 }
699
700 Some(sum / (len - (MIN_STOCH_LENGTH - 1)) as f64 * 100.0)
701 }
702}
703
704#[inline(always)]
705fn multi_length_stochastic_average_prepare<'a>(
706 input: &'a MultiLengthStochasticAverageInput,
707 kernel: Kernel,
708) -> Result<(&'a [f64], usize, ResolvedParams, Kernel), MultiLengthStochasticAverageError> {
709 let data = input.as_ref();
710 if data.is_empty() {
711 return Err(MultiLengthStochasticAverageError::EmptyInputData);
712 }
713
714 let first = first_valid_value(data);
715 if first >= data.len() {
716 return Err(MultiLengthStochasticAverageError::AllValuesNaN);
717 }
718
719 let params = resolve_params(&input.params, Some(data.len()))?;
720 let needed = total_warmup(params) + 1;
721 let valid = max_consecutive_valid_values(data);
722 if valid < needed {
723 return Err(MultiLengthStochasticAverageError::NotEnoughValidData { needed, valid });
724 }
725
726 let chosen = match kernel {
727 Kernel::Auto => detect_best_kernel(),
728 other => other.to_non_batch(),
729 };
730
731 Ok((data, first, params, chosen))
732}
733
734#[inline(always)]
735fn multi_length_stochastic_average_row_from_slice(
736 data: &[f64],
737 params: ResolvedParams,
738 out: &mut [f64],
739) {
740 out.fill(f64::NAN);
741 let mut stream = MultiLengthStochasticAverageStream::new_resolved(params);
742 for (slot, &value) in out.iter_mut().zip(data.iter()) {
743 if let Some(result) = stream.update(value) {
744 *slot = result;
745 }
746 }
747}
748
749#[inline]
750pub fn multi_length_stochastic_average(
751 input: &MultiLengthStochasticAverageInput,
752) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
753 multi_length_stochastic_average_with_kernel(input, Kernel::Auto)
754}
755
756#[inline]
757pub fn multi_length_stochastic_average_with_kernel(
758 input: &MultiLengthStochasticAverageInput,
759 kernel: Kernel,
760) -> Result<MultiLengthStochasticAverageOutput, MultiLengthStochasticAverageError> {
761 let (data, first, params, _chosen) = multi_length_stochastic_average_prepare(input, kernel)?;
762 let warmup = first.saturating_add(total_warmup(params)).min(data.len());
763 let mut values = alloc_with_nan_prefix(data.len(), warmup);
764 multi_length_stochastic_average_row_from_slice(data, params, &mut values);
765 Ok(MultiLengthStochasticAverageOutput { values })
766}
767
768#[inline]
769pub fn multi_length_stochastic_average_into_slice(
770 dst: &mut [f64],
771 input: &MultiLengthStochasticAverageInput,
772 kernel: Kernel,
773) -> Result<(), MultiLengthStochasticAverageError> {
774 let expected = input.as_ref().len();
775 if dst.len() != expected {
776 return Err(MultiLengthStochasticAverageError::OutputLengthMismatch {
777 expected,
778 got: dst.len(),
779 });
780 }
781 let (data, _first, params, _chosen) = multi_length_stochastic_average_prepare(input, kernel)?;
782 multi_length_stochastic_average_row_from_slice(data, params, dst);
783 Ok(())
784}
785
786#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
787#[inline]
788pub fn multi_length_stochastic_average_into(
789 input: &MultiLengthStochasticAverageInput,
790 out: &mut [f64],
791) -> Result<(), MultiLengthStochasticAverageError> {
792 multi_length_stochastic_average_into_slice(out, input, Kernel::Auto)
793}
794
795#[derive(Debug, Clone, PartialEq)]
796#[cfg_attr(
797 all(target_arch = "wasm32", feature = "wasm"),
798 derive(Serialize, Deserialize)
799)]
800pub struct MultiLengthStochasticAverageBatchRange {
801 pub length: (usize, usize, usize),
802 pub presmooth: (usize, usize, usize),
803 pub postsmooth: (usize, usize, usize),
804 pub premethod: Option<String>,
805 pub postmethod: Option<String>,
806}
807
808impl Default for MultiLengthStochasticAverageBatchRange {
809 fn default() -> Self {
810 Self {
811 length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
812 presmooth: (DEFAULT_PRESMOOTH, DEFAULT_PRESMOOTH, 0),
813 postsmooth: (DEFAULT_POSTSMOOTH, DEFAULT_POSTSMOOTH, 0),
814 premethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
815 postmethod: Some(DEFAULT_SMOOTHING_METHOD.to_string()),
816 }
817 }
818}
819
820#[derive(Clone, Debug)]
821pub struct MultiLengthStochasticAverageBatchOutput {
822 pub values: Vec<f64>,
823 pub combos: Vec<MultiLengthStochasticAverageParams>,
824 pub rows: usize,
825 pub cols: usize,
826}
827
828impl MultiLengthStochasticAverageBatchOutput {
829 #[inline]
830 pub fn row_for_params(&self, params: &MultiLengthStochasticAverageParams) -> Option<usize> {
831 let target = MultiLengthStochasticAverageParams {
832 length: Some(params.length.unwrap_or(DEFAULT_LENGTH)),
833 presmooth: Some(params.presmooth.unwrap_or(DEFAULT_PRESMOOTH)),
834 premethod: Some(canonical_method_name(
835 params.premethod.as_deref(),
836 DEFAULT_SMOOTHING_METHOD,
837 )),
838 postsmooth: Some(params.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH)),
839 postmethod: Some(canonical_method_name(
840 params.postmethod.as_deref(),
841 DEFAULT_SMOOTHING_METHOD,
842 )),
843 };
844 self.combos.iter().position(|combo| combo == &target)
845 }
846
847 #[inline]
848 pub fn values_for(&self, params: &MultiLengthStochasticAverageParams) -> Option<&[f64]> {
849 self.row_for_params(params).map(|row| {
850 let start = row * self.cols;
851 &self.values[start..start + self.cols]
852 })
853 }
854}
855
856#[derive(Clone, Debug)]
857pub struct MultiLengthStochasticAverageBatchBuilder {
858 range: MultiLengthStochasticAverageBatchRange,
859 kernel: Kernel,
860}
861
862impl Default for MultiLengthStochasticAverageBatchBuilder {
863 fn default() -> Self {
864 Self {
865 range: MultiLengthStochasticAverageBatchRange::default(),
866 kernel: Kernel::Auto,
867 }
868 }
869}
870
871impl MultiLengthStochasticAverageBatchBuilder {
872 #[inline]
873 pub fn new() -> Self {
874 Self::default()
875 }
876
877 #[inline]
878 pub fn kernel(mut self, kernel: Kernel) -> Self {
879 self.kernel = kernel;
880 self
881 }
882
883 #[inline]
884 pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
885 self.range.length = (start, end, step);
886 self
887 }
888
889 #[inline]
890 pub fn presmooth_range(mut self, start: usize, end: usize, step: usize) -> Self {
891 self.range.presmooth = (start, end, step);
892 self
893 }
894
895 #[inline]
896 pub fn postsmooth_range(mut self, start: usize, end: usize, step: usize) -> Self {
897 self.range.postsmooth = (start, end, step);
898 self
899 }
900
901 #[inline]
902 pub fn premethod<T: Into<String>>(mut self, premethod: T) -> Self {
903 self.range.premethod = Some(premethod.into());
904 self
905 }
906
907 #[inline]
908 pub fn postmethod<T: Into<String>>(mut self, postmethod: T) -> Self {
909 self.range.postmethod = Some(postmethod.into());
910 self
911 }
912
913 #[inline]
914 pub fn apply_slice(
915 self,
916 data: &[f64],
917 ) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
918 multi_length_stochastic_average_batch_with_kernel(data, &self.range, self.kernel)
919 }
920
921 #[inline]
922 pub fn apply_candles(
923 self,
924 candles: &Candles,
925 source: &str,
926 ) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
927 self.apply_slice(source_type(candles, source))
928 }
929}
930
931#[inline]
932fn expand_axis_usize(
933 start: usize,
934 end: usize,
935 step: usize,
936) -> Result<Vec<usize>, MultiLengthStochasticAverageError> {
937 if start == end {
938 return Ok(vec![start]);
939 }
940 if step == 0 {
941 return Err(MultiLengthStochasticAverageError::InvalidRange {
942 start: start.to_string(),
943 end: end.to_string(),
944 step: step.to_string(),
945 });
946 }
947
948 let mut out = Vec::new();
949 if start < end {
950 let mut value = start;
951 while value <= end {
952 out.push(value);
953 let next = value.saturating_add(step);
954 if next == value {
955 break;
956 }
957 value = next;
958 }
959 } else {
960 let mut value = start;
961 while value >= end {
962 out.push(value);
963 let next = value.saturating_sub(step);
964 if next == value {
965 break;
966 }
967 value = next;
968 }
969 }
970
971 if out.is_empty() {
972 return Err(MultiLengthStochasticAverageError::InvalidRange {
973 start: start.to_string(),
974 end: end.to_string(),
975 step: step.to_string(),
976 });
977 }
978
979 Ok(out)
980}
981
982fn expand_grid_multi_length_stochastic_average(
983 range: &MultiLengthStochasticAverageBatchRange,
984) -> Result<Vec<MultiLengthStochasticAverageParams>, MultiLengthStochasticAverageError> {
985 let lengths = expand_axis_usize(range.length.0, range.length.1, range.length.2)?;
986 let presmooths = expand_axis_usize(range.presmooth.0, range.presmooth.1, range.presmooth.2)?;
987 let postsmooths =
988 expand_axis_usize(range.postsmooth.0, range.postsmooth.1, range.postsmooth.2)?;
989 let premethod = canonical_method_name(range.premethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
990 let postmethod = canonical_method_name(range.postmethod.as_deref(), DEFAULT_SMOOTHING_METHOD);
991
992 let mut combos = Vec::with_capacity(
993 lengths
994 .len()
995 .saturating_mul(presmooths.len())
996 .saturating_mul(postsmooths.len()),
997 );
998
999 for &length in &lengths {
1000 for &presmooth in &presmooths {
1001 for &postsmooth in &postsmooths {
1002 combos.push(MultiLengthStochasticAverageParams {
1003 length: Some(length),
1004 presmooth: Some(presmooth),
1005 premethod: Some(premethod.clone()),
1006 postsmooth: Some(postsmooth),
1007 postmethod: Some(postmethod.clone()),
1008 });
1009 }
1010 }
1011 }
1012
1013 if combos.is_empty() {
1014 return Err(MultiLengthStochasticAverageError::InvalidRange {
1015 start: range.length.0.to_string(),
1016 end: range.length.1.to_string(),
1017 step: range.length.2.to_string(),
1018 });
1019 }
1020
1021 Ok(combos)
1022}
1023
1024#[inline]
1025pub fn multi_length_stochastic_average_batch_with_kernel(
1026 data: &[f64],
1027 sweep: &MultiLengthStochasticAverageBatchRange,
1028 kernel: Kernel,
1029) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1030 let batch = match kernel {
1031 Kernel::Auto => detect_best_batch_kernel(),
1032 other if other.is_batch() => other,
1033 other => {
1034 return Err(MultiLengthStochasticAverageError::InvalidKernelForBatch(
1035 other,
1036 ))
1037 }
1038 };
1039 multi_length_stochastic_average_batch_par_slice(data, sweep, batch.to_non_batch())
1040}
1041
1042#[inline]
1043pub fn multi_length_stochastic_average_batch_slice(
1044 data: &[f64],
1045 sweep: &MultiLengthStochasticAverageBatchRange,
1046 kernel: Kernel,
1047) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1048 multi_length_stochastic_average_batch_inner(data, sweep, kernel, false)
1049}
1050
1051#[inline]
1052pub fn multi_length_stochastic_average_batch_par_slice(
1053 data: &[f64],
1054 sweep: &MultiLengthStochasticAverageBatchRange,
1055 kernel: Kernel,
1056) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1057 multi_length_stochastic_average_batch_inner(data, sweep, kernel, true)
1058}
1059
1060pub fn multi_length_stochastic_average_batch_inner(
1061 data: &[f64],
1062 sweep: &MultiLengthStochasticAverageBatchRange,
1063 _kernel: Kernel,
1064 parallel: bool,
1065) -> Result<MultiLengthStochasticAverageBatchOutput, MultiLengthStochasticAverageError> {
1066 if data.is_empty() {
1067 return Err(MultiLengthStochasticAverageError::EmptyInputData);
1068 }
1069 let first = first_valid_value(data);
1070 if first >= data.len() {
1071 return Err(MultiLengthStochasticAverageError::AllValuesNaN);
1072 }
1073
1074 let combos = expand_grid_multi_length_stochastic_average(sweep)?;
1075 let max_valid = max_consecutive_valid_values(data);
1076 let rows = combos.len();
1077 let cols = data.len();
1078 let total =
1079 rows.checked_mul(cols)
1080 .ok_or(MultiLengthStochasticAverageError::OutputLengthMismatch {
1081 expected: usize::MAX,
1082 got: 0,
1083 })?;
1084
1085 let resolved = combos
1086 .iter()
1087 .map(|params| resolve_params(params, Some(cols)))
1088 .collect::<Result<Vec<_>, _>>()?;
1089
1090 for params in &resolved {
1091 let needed = total_warmup(*params) + 1;
1092 if max_valid < needed {
1093 return Err(MultiLengthStochasticAverageError::NotEnoughValidData {
1094 needed,
1095 valid: max_valid,
1096 });
1097 }
1098 }
1099
1100 let warmups = resolved
1101 .iter()
1102 .map(|params| first.saturating_add(total_warmup(*params)).min(cols))
1103 .collect::<Vec<_>>();
1104
1105 let mut values_mu = make_uninit_matrix(rows, cols);
1106 init_matrix_prefixes(&mut values_mu, cols, &warmups);
1107 let mut values_guard = ManuallyDrop::new(values_mu);
1108 let values_out =
1109 unsafe { std::slice::from_raw_parts_mut(values_guard.as_mut_ptr() as *mut f64, total) };
1110
1111 if parallel {
1112 #[cfg(not(target_arch = "wasm32"))]
1113 {
1114 values_out
1115 .par_chunks_mut(cols)
1116 .zip(resolved.par_iter())
1117 .for_each(|(row, params)| {
1118 multi_length_stochastic_average_row_from_slice(data, *params, row);
1119 });
1120 }
1121
1122 #[cfg(target_arch = "wasm32")]
1123 for (row, params) in resolved.iter().enumerate() {
1124 let start = row * cols;
1125 let end = start + cols;
1126 multi_length_stochastic_average_row_from_slice(
1127 data,
1128 *params,
1129 &mut values_out[start..end],
1130 );
1131 }
1132 } else {
1133 for (row, params) in resolved.iter().enumerate() {
1134 let start = row * cols;
1135 let end = start + cols;
1136 multi_length_stochastic_average_row_from_slice(
1137 data,
1138 *params,
1139 &mut values_out[start..end],
1140 );
1141 }
1142 }
1143
1144 let values = unsafe {
1145 Vec::from_raw_parts(
1146 values_guard.as_mut_ptr() as *mut f64,
1147 values_guard.len(),
1148 values_guard.capacity(),
1149 )
1150 };
1151 core::mem::forget(values_guard);
1152
1153 Ok(MultiLengthStochasticAverageBatchOutput {
1154 values,
1155 combos,
1156 rows,
1157 cols,
1158 })
1159}
1160
1161pub fn multi_length_stochastic_average_batch_inner_into(
1162 data: &[f64],
1163 sweep: &MultiLengthStochasticAverageBatchRange,
1164 kernel: Kernel,
1165 values_out: &mut [f64],
1166) -> Result<Vec<MultiLengthStochasticAverageParams>, MultiLengthStochasticAverageError> {
1167 let out = multi_length_stochastic_average_batch_inner(data, sweep, kernel, false)?;
1168 let total = out.rows * out.cols;
1169 if values_out.len() != total {
1170 return Err(MultiLengthStochasticAverageError::OutputLengthMismatch {
1171 expected: total,
1172 got: values_out.len(),
1173 });
1174 }
1175 values_out.copy_from_slice(&out.values);
1176 Ok(out.combos)
1177}
1178
1179#[cfg(feature = "python")]
1180#[pyfunction(name = "multi_length_stochastic_average")]
1181#[pyo3(signature = (
1182 data,
1183 length=DEFAULT_LENGTH,
1184 presmooth=DEFAULT_PRESMOOTH,
1185 premethod=DEFAULT_SMOOTHING_METHOD,
1186 postsmooth=DEFAULT_POSTSMOOTH,
1187 postmethod=DEFAULT_SMOOTHING_METHOD,
1188 kernel=None
1189))]
1190pub fn multi_length_stochastic_average_py<'py>(
1191 py: Python<'py>,
1192 data: PyReadonlyArray1<'py, f64>,
1193 length: usize,
1194 presmooth: usize,
1195 premethod: &str,
1196 postsmooth: usize,
1197 postmethod: &str,
1198 kernel: Option<&str>,
1199) -> PyResult<Bound<'py, PyArray1<f64>>> {
1200 let data = data.as_slice()?;
1201 let kernel = validate_kernel(kernel, false)?;
1202 let input = MultiLengthStochasticAverageInput::from_slice(
1203 data,
1204 MultiLengthStochasticAverageParams {
1205 length: Some(length),
1206 presmooth: Some(presmooth),
1207 premethod: Some(premethod.to_string()),
1208 postsmooth: Some(postsmooth),
1209 postmethod: Some(postmethod.to_string()),
1210 },
1211 );
1212 let out = py
1213 .allow_threads(|| multi_length_stochastic_average_with_kernel(&input, kernel))
1214 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1215 Ok(out.values.into_pyarray(py))
1216}
1217
1218#[cfg(feature = "python")]
1219#[pyclass(name = "MultiLengthStochasticAverageStream")]
1220pub struct MultiLengthStochasticAverageStreamPy {
1221 stream: MultiLengthStochasticAverageStream,
1222}
1223
1224#[cfg(feature = "python")]
1225#[pymethods]
1226impl MultiLengthStochasticAverageStreamPy {
1227 #[new]
1228 #[pyo3(signature = (
1229 length=DEFAULT_LENGTH,
1230 presmooth=DEFAULT_PRESMOOTH,
1231 premethod=DEFAULT_SMOOTHING_METHOD,
1232 postsmooth=DEFAULT_POSTSMOOTH,
1233 postmethod=DEFAULT_SMOOTHING_METHOD
1234 ))]
1235 fn new(
1236 length: usize,
1237 presmooth: usize,
1238 premethod: &str,
1239 postsmooth: usize,
1240 postmethod: &str,
1241 ) -> PyResult<Self> {
1242 let stream =
1243 MultiLengthStochasticAverageStream::try_new(MultiLengthStochasticAverageParams {
1244 length: Some(length),
1245 presmooth: Some(presmooth),
1246 premethod: Some(premethod.to_string()),
1247 postsmooth: Some(postsmooth),
1248 postmethod: Some(postmethod.to_string()),
1249 })
1250 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1251 Ok(Self { stream })
1252 }
1253
1254 fn update(&mut self, value: f64) -> Option<f64> {
1255 self.stream.update(value)
1256 }
1257
1258 #[getter]
1259 fn warmup_period(&self) -> usize {
1260 self.stream.get_warmup_period()
1261 }
1262}
1263
1264#[cfg(feature = "python")]
1265#[pyfunction(name = "multi_length_stochastic_average_batch")]
1266#[pyo3(signature = (
1267 data,
1268 length_range=(DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
1269 presmooth_range=(DEFAULT_PRESMOOTH, DEFAULT_PRESMOOTH, 0),
1270 premethod=DEFAULT_SMOOTHING_METHOD,
1271 postsmooth_range=(DEFAULT_POSTSMOOTH, DEFAULT_POSTSMOOTH, 0),
1272 postmethod=DEFAULT_SMOOTHING_METHOD,
1273 kernel=None
1274))]
1275pub fn multi_length_stochastic_average_batch_py<'py>(
1276 py: Python<'py>,
1277 data: PyReadonlyArray1<'py, f64>,
1278 length_range: (usize, usize, usize),
1279 presmooth_range: (usize, usize, usize),
1280 premethod: &str,
1281 postsmooth_range: (usize, usize, usize),
1282 postmethod: &str,
1283 kernel: Option<&str>,
1284) -> PyResult<Bound<'py, PyDict>> {
1285 let data = data.as_slice()?;
1286 let kernel = validate_kernel(kernel, true)?;
1287 let sweep = MultiLengthStochasticAverageBatchRange {
1288 length: length_range,
1289 presmooth: presmooth_range,
1290 postsmooth: postsmooth_range,
1291 premethod: Some(premethod.to_string()),
1292 postmethod: Some(postmethod.to_string()),
1293 };
1294 let combos = expand_grid_multi_length_stochastic_average(&sweep)
1295 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1296 let rows = combos.len();
1297 let cols = data.len();
1298 let total = rows
1299 .checked_mul(cols)
1300 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1301
1302 let values_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1303 let values_slice = unsafe { values_arr.as_slice_mut()? };
1304
1305 let combos = py
1306 .allow_threads(|| {
1307 let batch = match kernel {
1308 Kernel::Auto => detect_best_batch_kernel(),
1309 other => other,
1310 };
1311 multi_length_stochastic_average_batch_inner_into(
1312 data,
1313 &sweep,
1314 batch.to_non_batch(),
1315 values_slice,
1316 )
1317 })
1318 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1319
1320 let dict = PyDict::new(py);
1321 dict.set_item("values", values_arr.reshape((rows, cols))?)?;
1322 dict.set_item(
1323 "lengths",
1324 combos
1325 .iter()
1326 .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
1327 .collect::<Vec<_>>()
1328 .into_pyarray(py),
1329 )?;
1330 dict.set_item(
1331 "presmooths",
1332 combos
1333 .iter()
1334 .map(|combo| combo.presmooth.unwrap_or(DEFAULT_PRESMOOTH) as u64)
1335 .collect::<Vec<_>>()
1336 .into_pyarray(py),
1337 )?;
1338 dict.set_item(
1339 "postsmooths",
1340 combos
1341 .iter()
1342 .map(|combo| combo.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH) as u64)
1343 .collect::<Vec<_>>()
1344 .into_pyarray(py),
1345 )?;
1346 dict.set_item(
1347 "premethods",
1348 PyList::new(
1349 py,
1350 combos.iter().map(|combo| {
1351 combo
1352 .premethod
1353 .as_deref()
1354 .unwrap_or(DEFAULT_SMOOTHING_METHOD)
1355 }),
1356 )?,
1357 )?;
1358 dict.set_item(
1359 "postmethods",
1360 PyList::new(
1361 py,
1362 combos.iter().map(|combo| {
1363 combo
1364 .postmethod
1365 .as_deref()
1366 .unwrap_or(DEFAULT_SMOOTHING_METHOD)
1367 }),
1368 )?,
1369 )?;
1370 dict.set_item("rows", rows)?;
1371 dict.set_item("cols", cols)?;
1372 Ok(dict)
1373}
1374
1375#[cfg(feature = "python")]
1376pub fn register_multi_length_stochastic_average_module(
1377 module: &Bound<'_, pyo3::types::PyModule>,
1378) -> PyResult<()> {
1379 module.add_function(wrap_pyfunction!(
1380 multi_length_stochastic_average_py,
1381 module
1382 )?)?;
1383 module.add_function(wrap_pyfunction!(
1384 multi_length_stochastic_average_batch_py,
1385 module
1386 )?)?;
1387 module.add_class::<MultiLengthStochasticAverageStreamPy>()?;
1388 Ok(())
1389}
1390
1391#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1392#[derive(Serialize, Deserialize)]
1393pub struct MultiLengthStochasticAverageJsOutput {
1394 pub values: Vec<f64>,
1395}
1396
1397#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1398#[wasm_bindgen(js_name = "multi_length_stochastic_average_js")]
1399pub fn multi_length_stochastic_average_js(
1400 data: &[f64],
1401 length: usize,
1402 presmooth: usize,
1403 premethod: String,
1404 postsmooth: usize,
1405 postmethod: String,
1406) -> Result<JsValue, JsValue> {
1407 let input = MultiLengthStochasticAverageInput::from_slice(
1408 data,
1409 MultiLengthStochasticAverageParams {
1410 length: Some(length),
1411 presmooth: Some(presmooth),
1412 premethod: Some(premethod),
1413 postsmooth: Some(postsmooth),
1414 postmethod: Some(postmethod),
1415 },
1416 );
1417 let out =
1418 multi_length_stochastic_average(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1419 serde_wasm_bindgen::to_value(&MultiLengthStochasticAverageJsOutput { values: out.values })
1420 .map_err(|e| JsValue::from_str(&e.to_string()))
1421}
1422
1423#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1424#[wasm_bindgen]
1425pub fn multi_length_stochastic_average_alloc(len: usize) -> *mut f64 {
1426 let mut vec = Vec::<f64>::with_capacity(len);
1427 let ptr = vec.as_mut_ptr();
1428 std::mem::forget(vec);
1429 ptr
1430}
1431
1432#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1433#[wasm_bindgen]
1434pub fn multi_length_stochastic_average_free(ptr: *mut f64, len: usize) {
1435 if !ptr.is_null() {
1436 unsafe {
1437 let _ = Vec::from_raw_parts(ptr, len, len);
1438 }
1439 }
1440}
1441
1442#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1443#[wasm_bindgen]
1444pub fn multi_length_stochastic_average_into(
1445 data_ptr: *const f64,
1446 values_ptr: *mut f64,
1447 len: usize,
1448 length: usize,
1449 presmooth: usize,
1450 premethod: String,
1451 postsmooth: usize,
1452 postmethod: String,
1453) -> Result<(), JsValue> {
1454 if data_ptr.is_null() || values_ptr.is_null() {
1455 return Err(JsValue::from_str("Null pointer provided"));
1456 }
1457
1458 unsafe {
1459 let data = std::slice::from_raw_parts(data_ptr, len);
1460 let input = MultiLengthStochasticAverageInput::from_slice(
1461 data,
1462 MultiLengthStochasticAverageParams {
1463 length: Some(length),
1464 presmooth: Some(presmooth),
1465 premethod: Some(premethod),
1466 postsmooth: Some(postsmooth),
1467 postmethod: Some(postmethod),
1468 },
1469 );
1470 if data_ptr == values_ptr {
1471 let mut tmp = vec![0.0; len];
1472 multi_length_stochastic_average_into_slice(&mut tmp, &input, Kernel::Auto)
1473 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1474 std::slice::from_raw_parts_mut(values_ptr, len).copy_from_slice(&tmp);
1475 } else {
1476 multi_length_stochastic_average_into_slice(
1477 std::slice::from_raw_parts_mut(values_ptr, len),
1478 &input,
1479 Kernel::Auto,
1480 )
1481 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1482 }
1483 }
1484 Ok(())
1485}
1486
1487#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1488#[derive(Serialize, Deserialize)]
1489pub struct MultiLengthStochasticAverageBatchJsConfig {
1490 pub length_range: (usize, usize, usize),
1491 pub presmooth_range: Option<(usize, usize, usize)>,
1492 pub premethod: Option<String>,
1493 pub postsmooth_range: Option<(usize, usize, usize)>,
1494 pub postmethod: Option<String>,
1495}
1496
1497#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1498#[derive(Serialize, Deserialize)]
1499pub struct MultiLengthStochasticAverageBatchJsOutput {
1500 pub values: Vec<f64>,
1501 pub combos: Vec<MultiLengthStochasticAverageParams>,
1502 pub lengths: Vec<usize>,
1503 pub presmooths: Vec<usize>,
1504 pub postsmooths: Vec<usize>,
1505 pub premethods: Vec<String>,
1506 pub postmethods: Vec<String>,
1507 pub rows: usize,
1508 pub cols: usize,
1509}
1510
1511#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1512#[wasm_bindgen(js_name = "multi_length_stochastic_average_batch_js")]
1513pub fn multi_length_stochastic_average_batch_js(
1514 data: &[f64],
1515 config: JsValue,
1516) -> Result<JsValue, JsValue> {
1517 let config: MultiLengthStochasticAverageBatchJsConfig = serde_wasm_bindgen::from_value(config)
1518 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1519 let sweep = MultiLengthStochasticAverageBatchRange {
1520 length: config.length_range,
1521 presmooth: config
1522 .presmooth_range
1523 .unwrap_or((DEFAULT_PRESMOOTH, DEFAULT_PRESMOOTH, 0)),
1524 premethod: config
1525 .premethod
1526 .or_else(|| Some(DEFAULT_SMOOTHING_METHOD.to_string())),
1527 postsmooth: config
1528 .postsmooth_range
1529 .unwrap_or((DEFAULT_POSTSMOOTH, DEFAULT_POSTSMOOTH, 0)),
1530 postmethod: config
1531 .postmethod
1532 .or_else(|| Some(DEFAULT_SMOOTHING_METHOD.to_string())),
1533 };
1534 let out = multi_length_stochastic_average_batch_inner(
1535 data,
1536 &sweep,
1537 detect_best_batch_kernel().to_non_batch(),
1538 false,
1539 )
1540 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1541
1542 serde_wasm_bindgen::to_value(&MultiLengthStochasticAverageBatchJsOutput {
1543 lengths: out
1544 .combos
1545 .iter()
1546 .map(|p| p.length.unwrap_or(DEFAULT_LENGTH))
1547 .collect(),
1548 presmooths: out
1549 .combos
1550 .iter()
1551 .map(|p| p.presmooth.unwrap_or(DEFAULT_PRESMOOTH))
1552 .collect(),
1553 postsmooths: out
1554 .combos
1555 .iter()
1556 .map(|p| p.postsmooth.unwrap_or(DEFAULT_POSTSMOOTH))
1557 .collect(),
1558 premethods: out
1559 .combos
1560 .iter()
1561 .map(|p| canonical_method_name(p.premethod.as_deref(), DEFAULT_SMOOTHING_METHOD))
1562 .collect(),
1563 postmethods: out
1564 .combos
1565 .iter()
1566 .map(|p| canonical_method_name(p.postmethod.as_deref(), DEFAULT_SMOOTHING_METHOD))
1567 .collect(),
1568 values: out.values,
1569 combos: out.combos,
1570 rows: out.rows,
1571 cols: out.cols,
1572 })
1573 .map_err(|e| JsValue::from_str(&e.to_string()))
1574}
1575
1576#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1577#[wasm_bindgen]
1578pub fn multi_length_stochastic_average_batch_into(
1579 data_ptr: *const f64,
1580 values_ptr: *mut f64,
1581 len: usize,
1582 length_start: usize,
1583 length_end: usize,
1584 length_step: usize,
1585 presmooth_start: usize,
1586 presmooth_end: usize,
1587 presmooth_step: usize,
1588 premethod: String,
1589 postsmooth_start: usize,
1590 postsmooth_end: usize,
1591 postsmooth_step: usize,
1592 postmethod: String,
1593) -> Result<usize, JsValue> {
1594 if data_ptr.is_null() || values_ptr.is_null() {
1595 return Err(JsValue::from_str("Null pointer provided"));
1596 }
1597
1598 let sweep = MultiLengthStochasticAverageBatchRange {
1599 length: (length_start, length_end, length_step),
1600 presmooth: (presmooth_start, presmooth_end, presmooth_step),
1601 premethod: Some(premethod),
1602 postsmooth: (postsmooth_start, postsmooth_end, postsmooth_step),
1603 postmethod: Some(postmethod),
1604 };
1605 let combos = expand_grid_multi_length_stochastic_average(&sweep)
1606 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1607 let rows = combos.len();
1608 let total = rows
1609 .checked_mul(len)
1610 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1611
1612 unsafe {
1613 let data = std::slice::from_raw_parts(data_ptr, len);
1614 let values_out = std::slice::from_raw_parts_mut(values_ptr, total);
1615 multi_length_stochastic_average_batch_inner_into(
1616 data,
1617 &sweep,
1618 detect_best_batch_kernel().to_non_batch(),
1619 values_out,
1620 )
1621 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1622 }
1623 Ok(rows)
1624}
1625
1626#[cfg(test)]
1627mod tests {
1628 use super::*;
1629 use std::error::Error;
1630
1631 fn sample_source(len: usize) -> Vec<f64> {
1632 (0..len)
1633 .map(|i| {
1634 100.0
1635 + i as f64 * 0.04
1636 + (i as f64 * 0.17).sin() * 1.8
1637 + (i as f64 * 0.03).cos() * 0.4
1638 })
1639 .collect()
1640 }
1641
1642 fn sample_candles(len: usize) -> Candles {
1643 let open: Vec<f64> = (0..len)
1644 .map(|i| 100.0 + i as f64 * 0.03 + (i as f64 * 0.11).sin() * 1.2)
1645 .collect();
1646 let close: Vec<f64> = open
1647 .iter()
1648 .enumerate()
1649 .map(|(i, &o)| o + (i as f64 * 0.19).cos() * 0.7)
1650 .collect();
1651 let high: Vec<f64> = open
1652 .iter()
1653 .zip(close.iter())
1654 .enumerate()
1655 .map(|(i, (&o, &c))| o.max(c) + 0.4 + (i as f64 * 0.07).sin().abs() * 0.2)
1656 .collect();
1657 let low: Vec<f64> = open
1658 .iter()
1659 .zip(close.iter())
1660 .enumerate()
1661 .map(|(i, (&o, &c))| o.min(c) - 0.4 - (i as f64 * 0.05).cos().abs() * 0.2)
1662 .collect();
1663 Candles::new(
1664 (0..len as i64).collect(),
1665 open,
1666 high,
1667 low,
1668 close,
1669 vec![1_000.0; len],
1670 )
1671 }
1672
1673 fn assert_series_eq(lhs: &[f64], rhs: &[f64], tol: f64) {
1674 assert_eq!(lhs.len(), rhs.len());
1675 for (i, (&left, &right)) in lhs.iter().zip(rhs.iter()).enumerate() {
1676 if left.is_nan() && right.is_nan() {
1677 continue;
1678 }
1679 assert!(
1680 (left - right).abs() <= tol,
1681 "mismatch at index {i}: left={left}, right={right}, tol={tol}"
1682 );
1683 }
1684 }
1685
1686 #[test]
1687 fn multi_length_stochastic_average_output_contract() -> Result<(), Box<dyn Error>> {
1688 let data = sample_source(256);
1689 let out = multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1690 &data,
1691 MultiLengthStochasticAverageParams::default(),
1692 ))?;
1693
1694 assert_eq!(out.values.len(), data.len());
1695 let first_finite = out
1696 .values
1697 .iter()
1698 .position(|value| value.is_finite())
1699 .unwrap();
1700 assert!(first_finite >= 22);
1701 for &value in out.values.iter().filter(|value| value.is_finite()) {
1702 assert!((-1e-9..=100.0 + 1e-9).contains(&value));
1703 }
1704 Ok(())
1705 }
1706
1707 #[test]
1708 fn multi_length_stochastic_average_rejects_invalid_parameters() {
1709 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1710
1711 let err = multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1712 &data,
1713 MultiLengthStochasticAverageParams {
1714 length: Some(3),
1715 ..MultiLengthStochasticAverageParams::default()
1716 },
1717 ))
1718 .unwrap_err();
1719 assert!(matches!(
1720 err,
1721 MultiLengthStochasticAverageError::InvalidLength { length: 3, .. }
1722 ));
1723
1724 let err = multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1725 &data,
1726 MultiLengthStochasticAverageParams {
1727 premethod: Some("ema".to_string()),
1728 ..MultiLengthStochasticAverageParams::default()
1729 },
1730 ))
1731 .unwrap_err();
1732 assert!(matches!(
1733 err,
1734 MultiLengthStochasticAverageError::InvalidPreMethod { .. }
1735 ));
1736 }
1737
1738 #[test]
1739 fn multi_length_stochastic_average_builder_supports_candles() -> Result<(), Box<dyn Error>> {
1740 let candles = sample_candles(220);
1741 let built = MultiLengthStochasticAverageBuilder::new()
1742 .length(12)
1743 .presmooth(5)
1744 .premethod("lsma")
1745 .postsmooth(4)
1746 .postmethod("sma")
1747 .apply(&candles, "hlc3")?;
1748
1749 let direct =
1750 multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_candles(
1751 &candles,
1752 "hlc3",
1753 MultiLengthStochasticAverageParams {
1754 length: Some(12),
1755 presmooth: Some(5),
1756 premethod: Some("lsma".to_string()),
1757 postsmooth: Some(4),
1758 postmethod: Some("sma".to_string()),
1759 },
1760 ))?;
1761
1762 assert_series_eq(&built.values, &direct.values, 1e-12);
1763 Ok(())
1764 }
1765
1766 #[test]
1767 fn multi_length_stochastic_average_stream_matches_batch_with_reset(
1768 ) -> Result<(), Box<dyn Error>> {
1769 let mut data = sample_source(220);
1770 data[110] = f64::NAN;
1771 let params = MultiLengthStochasticAverageParams {
1772 length: Some(12),
1773 presmooth: Some(5),
1774 premethod: Some("lsma".to_string()),
1775 postsmooth: Some(4),
1776 postmethod: Some("sma".to_string()),
1777 };
1778
1779 let batch = multi_length_stochastic_average(
1780 &MultiLengthStochasticAverageInput::from_slice(&data, params.clone()),
1781 )?;
1782 let mut stream = MultiLengthStochasticAverageStream::try_new(params)?;
1783 let mut streamed = Vec::with_capacity(data.len());
1784
1785 for &value in &data {
1786 streamed.push(stream.update(value).unwrap_or(f64::NAN));
1787 }
1788
1789 assert_eq!(stream.get_warmup_period(), 18);
1790 assert_series_eq(&batch.values, &streamed, 1e-12);
1791 Ok(())
1792 }
1793
1794 #[test]
1795 fn multi_length_stochastic_average_into_matches_api() -> Result<(), Box<dyn Error>> {
1796 let data = sample_source(192);
1797 let input = MultiLengthStochasticAverageInput::from_slice(
1798 &data,
1799 MultiLengthStochasticAverageParams {
1800 length: Some(16),
1801 presmooth: Some(6),
1802 premethod: Some("tma".to_string()),
1803 postsmooth: Some(5),
1804 postmethod: Some("lsma".to_string()),
1805 },
1806 );
1807 let api = multi_length_stochastic_average(&input)?;
1808 let mut out = vec![f64::NAN; data.len()];
1809 multi_length_stochastic_average_into_slice(&mut out, &input, Kernel::Auto)?;
1810 assert_series_eq(&api.values, &out, 1e-12);
1811 Ok(())
1812 }
1813
1814 #[test]
1815 fn multi_length_stochastic_average_batch_single_param_matches_single(
1816 ) -> Result<(), Box<dyn Error>> {
1817 let data = sample_source(128);
1818 let sweep = MultiLengthStochasticAverageBatchRange {
1819 length: (12, 12, 0),
1820 presmooth: (5, 5, 0),
1821 premethod: Some("lsma".to_string()),
1822 postsmooth: (4, 4, 0),
1823 postmethod: Some("sma".to_string()),
1824 };
1825 let batch = multi_length_stochastic_average_batch_with_kernel(&data, &sweep, Kernel::Auto)?;
1826 let single =
1827 multi_length_stochastic_average(&MultiLengthStochasticAverageInput::from_slice(
1828 &data,
1829 MultiLengthStochasticAverageParams {
1830 length: Some(12),
1831 presmooth: Some(5),
1832 premethod: Some("lsma".to_string()),
1833 postsmooth: Some(4),
1834 postmethod: Some("sma".to_string()),
1835 },
1836 ))?;
1837
1838 assert_eq!(batch.rows, 1);
1839 assert_eq!(batch.cols, data.len());
1840 assert_eq!(batch.combos[0].length, Some(12));
1841 assert_eq!(batch.combos[0].presmooth, Some(5));
1842 assert_eq!(batch.combos[0].postsmooth, Some(4));
1843 assert_eq!(batch.combos[0].premethod.as_deref(), Some("lsma"));
1844 assert_eq!(batch.combos[0].postmethod.as_deref(), Some("sma"));
1845 assert_series_eq(&batch.values[..data.len()], &single.values, 1e-12);
1846 Ok(())
1847 }
1848
1849 #[test]
1850 fn multi_length_stochastic_average_batch_metadata() -> Result<(), Box<dyn Error>> {
1851 let data = sample_source(96);
1852 let sweep = MultiLengthStochasticAverageBatchRange {
1853 length: (10, 14, 2),
1854 presmooth: (4, 6, 2),
1855 premethod: Some("sma".to_string()),
1856 postsmooth: (3, 5, 2),
1857 postmethod: Some("lsma".to_string()),
1858 };
1859 let batch = multi_length_stochastic_average_batch_with_kernel(&data, &sweep, Kernel::Auto)?;
1860
1861 assert_eq!(batch.rows, 12);
1862 assert_eq!(batch.cols, data.len());
1863 assert_eq!(batch.combos.len(), 12);
1864 assert_eq!(batch.values.len(), 12 * data.len());
1865 for combo in &batch.combos {
1866 assert_eq!(combo.premethod.as_deref(), Some("sma"));
1867 assert_eq!(combo.postmethod.as_deref(), Some("lsma"));
1868 }
1869 Ok(())
1870 }
1871}