1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(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::collections::VecDeque;
28use std::convert::AsRef;
29use std::mem::ManuallyDrop;
30use thiserror::Error;
31
32const DEFAULT_LOOKBACK_LENGTH: usize = 200;
33const DEFAULT_LENGTH1: usize = 12;
34const DEFAULT_LENGTH2: usize = 3;
35const DEFAULT_OB_LEVEL: i32 = 40;
36const DEFAULT_OS_LEVEL: i32 = -40;
37const FLOAT_TOL: f64 = 1e-12;
38
39impl<'a> AsRef<[f64]> for StochasticDistanceInput<'a> {
40 #[inline(always)]
41 fn as_ref(&self) -> &[f64] {
42 match &self.data {
43 StochasticDistanceData::Slice(slice) => slice,
44 StochasticDistanceData::Candles { candles } => source_type(candles, "close"),
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
50pub enum StochasticDistanceData<'a> {
51 Candles { candles: &'a Candles },
52 Slice(&'a [f64]),
53}
54
55#[derive(Debug, Clone)]
56pub struct StochasticDistanceOutput {
57 pub oscillator: Vec<f64>,
58 pub signal: Vec<f64>,
59}
60
61#[derive(Debug, Clone, PartialEq)]
62#[cfg_attr(
63 all(target_arch = "wasm32", feature = "wasm"),
64 derive(Serialize, Deserialize)
65)]
66pub struct StochasticDistanceParams {
67 pub lookback_length: Option<usize>,
68 pub length1: Option<usize>,
69 pub length2: Option<usize>,
70 pub ob_level: Option<i32>,
71 pub os_level: Option<i32>,
72}
73
74impl Default for StochasticDistanceParams {
75 fn default() -> Self {
76 Self {
77 lookback_length: Some(DEFAULT_LOOKBACK_LENGTH),
78 length1: Some(DEFAULT_LENGTH1),
79 length2: Some(DEFAULT_LENGTH2),
80 ob_level: Some(DEFAULT_OB_LEVEL),
81 os_level: Some(DEFAULT_OS_LEVEL),
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
87pub struct StochasticDistanceInput<'a> {
88 pub data: StochasticDistanceData<'a>,
89 pub params: StochasticDistanceParams,
90}
91
92impl<'a> StochasticDistanceInput<'a> {
93 #[inline]
94 pub fn from_candles(candles: &'a Candles, params: StochasticDistanceParams) -> Self {
95 Self {
96 data: StochasticDistanceData::Candles { candles },
97 params,
98 }
99 }
100
101 #[inline]
102 pub fn from_slice(slice: &'a [f64], params: StochasticDistanceParams) -> Self {
103 Self {
104 data: StochasticDistanceData::Slice(slice),
105 params,
106 }
107 }
108
109 #[inline]
110 pub fn with_default_candles(candles: &'a Candles) -> Self {
111 Self::from_candles(candles, StochasticDistanceParams::default())
112 }
113}
114
115#[derive(Clone, Debug)]
116pub struct StochasticDistanceBuilder {
117 lookback_length: Option<usize>,
118 length1: Option<usize>,
119 length2: Option<usize>,
120 ob_level: Option<i32>,
121 os_level: Option<i32>,
122 kernel: Kernel,
123}
124
125impl Default for StochasticDistanceBuilder {
126 fn default() -> Self {
127 Self {
128 lookback_length: None,
129 length1: None,
130 length2: None,
131 ob_level: None,
132 os_level: None,
133 kernel: Kernel::Auto,
134 }
135 }
136}
137
138impl StochasticDistanceBuilder {
139 #[inline]
140 pub fn new() -> Self {
141 Self::default()
142 }
143
144 #[inline]
145 pub fn lookback_length(mut self, lookback_length: usize) -> Self {
146 self.lookback_length = Some(lookback_length);
147 self
148 }
149
150 #[inline]
151 pub fn length1(mut self, length1: usize) -> Self {
152 self.length1 = Some(length1);
153 self
154 }
155
156 #[inline]
157 pub fn length2(mut self, length2: usize) -> Self {
158 self.length2 = Some(length2);
159 self
160 }
161
162 #[inline]
163 pub fn ob_level(mut self, ob_level: i32) -> Self {
164 self.ob_level = Some(ob_level);
165 self
166 }
167
168 #[inline]
169 pub fn os_level(mut self, os_level: i32) -> Self {
170 self.os_level = Some(os_level);
171 self
172 }
173
174 #[inline]
175 pub fn kernel(mut self, kernel: Kernel) -> Self {
176 self.kernel = kernel;
177 self
178 }
179
180 #[inline]
181 pub fn apply(
182 self,
183 candles: &Candles,
184 ) -> Result<StochasticDistanceOutput, StochasticDistanceError> {
185 let input = StochasticDistanceInput::from_candles(
186 candles,
187 StochasticDistanceParams {
188 lookback_length: self.lookback_length,
189 length1: self.length1,
190 length2: self.length2,
191 ob_level: self.ob_level,
192 os_level: self.os_level,
193 },
194 );
195 stochastic_distance_with_kernel(&input, self.kernel)
196 }
197
198 #[inline]
199 pub fn apply_slice(
200 self,
201 data: &[f64],
202 ) -> Result<StochasticDistanceOutput, StochasticDistanceError> {
203 let input = StochasticDistanceInput::from_slice(
204 data,
205 StochasticDistanceParams {
206 lookback_length: self.lookback_length,
207 length1: self.length1,
208 length2: self.length2,
209 ob_level: self.ob_level,
210 os_level: self.os_level,
211 },
212 );
213 stochastic_distance_with_kernel(&input, self.kernel)
214 }
215
216 #[inline]
217 pub fn into_stream(self) -> Result<StochasticDistanceStream, StochasticDistanceError> {
218 StochasticDistanceStream::try_new(StochasticDistanceParams {
219 lookback_length: self.lookback_length,
220 length1: self.length1,
221 length2: self.length2,
222 ob_level: self.ob_level,
223 os_level: self.os_level,
224 })
225 }
226}
227
228#[derive(Debug, Error)]
229pub enum StochasticDistanceError {
230 #[error("stochastic_distance: Input data slice is empty.")]
231 EmptyInputData,
232 #[error("stochastic_distance: All values are NaN.")]
233 AllValuesNaN,
234 #[error(
235 "stochastic_distance: Invalid lookback_length: lookback_length = {lookback_length}, data length = {data_len}"
236 )]
237 InvalidLookbackLength {
238 lookback_length: usize,
239 data_len: usize,
240 },
241 #[error("stochastic_distance: Invalid length1: length1 = {length1}, data length = {data_len}")]
242 InvalidLength1 { length1: usize, data_len: usize },
243 #[error("stochastic_distance: Invalid length2: {length2}")]
244 InvalidLength2 { length2: usize },
245 #[error("stochastic_distance: Invalid ob_level: {ob_level}")]
246 InvalidObLevel { ob_level: i32 },
247 #[error("stochastic_distance: Invalid os_level: {os_level}")]
248 InvalidOsLevel { os_level: i32 },
249 #[error(
250 "stochastic_distance: Invalid thresholds: os_level ({os_level}) must be less than ob_level ({ob_level})"
251 )]
252 InvalidThresholdOrder { ob_level: i32, os_level: i32 },
253 #[error("stochastic_distance: Not enough valid data: needed = {needed}, valid = {valid}")]
254 NotEnoughValidData { needed: usize, valid: usize },
255 #[error("stochastic_distance: Output length mismatch: expected = {expected}, oscillator = {oscillator_got}, signal = {signal_got}")]
256 OutputLengthMismatch {
257 expected: usize,
258 oscillator_got: usize,
259 signal_got: usize,
260 },
261 #[error("stochastic_distance: Invalid range: start={start}, end={end}, step={step}")]
262 InvalidRange {
263 start: String,
264 end: String,
265 step: String,
266 },
267 #[error("stochastic_distance: Invalid kernel for batch: {0:?}")]
268 InvalidKernelForBatch(Kernel),
269}
270
271#[derive(Debug, Clone, Copy)]
272struct ResolvedParams {
273 lookback_length: usize,
274 length1: usize,
275 length2: usize,
276 ob_level: f64,
277 os_level: f64,
278}
279
280#[inline(always)]
281fn first_valid_value(data: &[f64]) -> usize {
282 let mut i = 0usize;
283 while i < data.len() {
284 if data[i].is_finite() {
285 break;
286 }
287 i += 1;
288 }
289 i.min(data.len())
290}
291
292#[inline(always)]
293fn count_valid_values(data: &[f64]) -> usize {
294 data.iter().filter(|v| v.is_finite()).count()
295}
296
297#[inline(always)]
298fn warmup_period(params: ResolvedParams) -> usize {
299 params.length1 + params.lookback_length - 1
300}
301
302#[inline]
303fn resolve_params(
304 params: &StochasticDistanceParams,
305 data_len: Option<usize>,
306) -> Result<ResolvedParams, StochasticDistanceError> {
307 let lookback_length = params.lookback_length.unwrap_or(DEFAULT_LOOKBACK_LENGTH);
308 let length1 = params.length1.unwrap_or(DEFAULT_LENGTH1);
309 let length2 = params.length2.unwrap_or(DEFAULT_LENGTH2);
310 let ob_level = params.ob_level.unwrap_or(DEFAULT_OB_LEVEL);
311 let os_level = params.os_level.unwrap_or(DEFAULT_OS_LEVEL);
312
313 if lookback_length == 0 {
314 return Err(StochasticDistanceError::InvalidLookbackLength {
315 lookback_length,
316 data_len: data_len.unwrap_or(0),
317 });
318 }
319 if length1 == 0 {
320 return Err(StochasticDistanceError::InvalidLength1 {
321 length1,
322 data_len: data_len.unwrap_or(0),
323 });
324 }
325 if length2 == 0 {
326 return Err(StochasticDistanceError::InvalidLength2 { length2 });
327 }
328 if !(0..=100).contains(&ob_level) {
329 return Err(StochasticDistanceError::InvalidObLevel { ob_level });
330 }
331 if !(-100..=0).contains(&os_level) {
332 return Err(StochasticDistanceError::InvalidOsLevel { os_level });
333 }
334 if os_level >= ob_level {
335 return Err(StochasticDistanceError::InvalidThresholdOrder { ob_level, os_level });
336 }
337
338 if let Some(data_len) = data_len {
339 if lookback_length > data_len {
340 return Err(StochasticDistanceError::InvalidLookbackLength {
341 lookback_length,
342 data_len,
343 });
344 }
345 if length1 > data_len {
346 return Err(StochasticDistanceError::InvalidLength1 { length1, data_len });
347 }
348 }
349
350 Ok(ResolvedParams {
351 lookback_length,
352 length1,
353 length2,
354 ob_level: ob_level as f64,
355 os_level: os_level as f64,
356 })
357}
358
359#[derive(Debug, Clone)]
360pub struct StochasticDistanceStream {
361 params: ResolvedParams,
362 close_ring: Vec<f64>,
363 close_head: usize,
364 close_count: usize,
365 dist_index: usize,
366 max_deque: VecDeque<(usize, f64)>,
367 min_deque: VecDeque<(usize, f64)>,
368 ema: f64,
369 have_ema: bool,
370 prev_sdo: f64,
371}
372
373impl StochasticDistanceStream {
374 pub fn try_new(params: StochasticDistanceParams) -> Result<Self, StochasticDistanceError> {
375 let params = resolve_params(¶ms, None)?;
376 Ok(Self::new_resolved(params))
377 }
378
379 #[inline]
380 fn new_resolved(params: ResolvedParams) -> Self {
381 Self {
382 params,
383 close_ring: vec![0.0; params.length1.max(1)],
384 close_head: 0,
385 close_count: 0,
386 dist_index: 0,
387 max_deque: VecDeque::with_capacity(params.lookback_length),
388 min_deque: VecDeque::with_capacity(params.lookback_length),
389 ema: 0.0,
390 have_ema: false,
391 prev_sdo: 0.0,
392 }
393 }
394
395 #[inline]
396 fn reset(&mut self) {
397 *self = Self::new_resolved(self.params);
398 }
399
400 #[inline]
401 pub fn get_warmup_period(&self) -> usize {
402 warmup_period(self.params)
403 }
404
405 #[inline]
406 pub fn update(&mut self, close: f64) -> Option<(f64, f64)> {
407 if !close.is_finite() {
408 self.reset();
409 return None;
410 }
411
412 let lag_close = if self.close_count >= self.params.length1 {
413 Some(self.close_ring[self.close_head])
414 } else {
415 None
416 };
417
418 self.close_ring[self.close_head] = close;
419 self.close_head += 1;
420 if self.close_head == self.params.length1 {
421 self.close_head = 0;
422 }
423 if self.close_count < self.params.length1 {
424 self.close_count += 1;
425 }
426
427 let lag_close = lag_close?;
428 let distance = (close - lag_close).abs();
429 self.push_distance(distance, close, lag_close)
430 }
431
432 #[inline]
433 fn push_distance(&mut self, distance: f64, close: f64, lag_close: f64) -> Option<(f64, f64)> {
434 let idx = self.dist_index;
435 self.dist_index += 1;
436
437 while matches!(self.max_deque.back(), Some((_, v)) if *v <= distance) {
438 self.max_deque.pop_back();
439 }
440 self.max_deque.push_back((idx, distance));
441 while matches!(self.min_deque.back(), Some((_, v)) if *v >= distance) {
442 self.min_deque.pop_back();
443 }
444 self.min_deque.push_back((idx, distance));
445
446 let window = self.params.lookback_length;
447 let cutoff = idx.saturating_sub(window.saturating_sub(1));
448 while matches!(self.max_deque.front(), Some((front_idx, _)) if *front_idx < cutoff) {
449 self.max_deque.pop_front();
450 }
451 while matches!(self.min_deque.front(), Some((front_idx, _)) if *front_idx < cutoff) {
452 self.min_deque.pop_front();
453 }
454
455 if idx + 1 < window {
456 return None;
457 }
458
459 let hh = self.max_deque.front().map(|(_, v)| *v).unwrap_or(distance);
460 let ll = self.min_deque.front().map(|(_, v)| *v).unwrap_or(distance);
461 let spread = hh - ll;
462 let distance_sto = if spread.abs() > FLOAT_TOL {
463 (distance - ll) / spread * 100.0
464 } else {
465 0.0
466 };
467 let distance_d = if close > lag_close + FLOAT_TOL {
468 distance_sto
469 } else if close + FLOAT_TOL < lag_close {
470 -distance_sto
471 } else {
472 0.0
473 };
474
475 let alpha = 2.0 / (self.params.length2 as f64 + 1.0);
476 if self.have_ema {
477 self.ema = alpha * distance_d + (1.0 - alpha) * self.ema;
478 } else {
479 self.ema = distance_d;
480 self.have_ema = true;
481 }
482
483 let signal = if distance_d > self.ema
484 || (self.prev_sdo < self.params.os_level && self.ema > self.params.os_level)
485 {
486 1.0
487 } else if distance_d < self.ema
488 || (self.prev_sdo > self.params.ob_level && self.ema < self.params.ob_level)
489 {
490 -1.0
491 } else {
492 0.0
493 };
494 self.prev_sdo = self.ema;
495
496 Some((self.ema, signal))
497 }
498}
499
500#[inline]
501pub fn stochastic_distance(
502 input: &StochasticDistanceInput,
503) -> Result<StochasticDistanceOutput, StochasticDistanceError> {
504 stochastic_distance_with_kernel(input, Kernel::Auto)
505}
506
507#[inline(always)]
508fn stochastic_distance_row_from_slice(
509 data: &[f64],
510 params: ResolvedParams,
511 oscillator_out: &mut [f64],
512 signal_out: &mut [f64],
513) {
514 let mut stream = StochasticDistanceStream::new_resolved(params);
515 for i in 0..data.len() {
516 match stream.update(data[i]) {
517 Some((oscillator, signal)) => {
518 oscillator_out[i] = oscillator;
519 signal_out[i] = signal;
520 }
521 None => {
522 oscillator_out[i] = f64::NAN;
523 signal_out[i] = f64::NAN;
524 }
525 }
526 }
527}
528
529#[inline(always)]
530fn stochastic_distance_prepare<'a>(
531 input: &'a StochasticDistanceInput,
532 kernel: Kernel,
533) -> Result<(&'a [f64], usize, usize, ResolvedParams, Kernel), StochasticDistanceError> {
534 let data = input.as_ref();
535 if data.is_empty() {
536 return Err(StochasticDistanceError::EmptyInputData);
537 }
538 let first = first_valid_value(data);
539 if first >= data.len() {
540 return Err(StochasticDistanceError::AllValuesNaN);
541 }
542
543 let params = resolve_params(&input.params, Some(data.len()))?;
544 let valid = count_valid_values(data);
545 let needed = params.lookback_length + params.length1;
546 if valid < needed {
547 return Err(StochasticDistanceError::NotEnoughValidData { needed, valid });
548 }
549
550 let chosen = match kernel {
551 Kernel::Auto => detect_best_kernel(),
552 other => other.to_non_batch(),
553 };
554 Ok((data, first, valid, params, chosen))
555}
556
557pub fn stochastic_distance_with_kernel(
558 input: &StochasticDistanceInput,
559 kernel: Kernel,
560) -> Result<StochasticDistanceOutput, StochasticDistanceError> {
561 let (data, first, _valid, params, _chosen) = stochastic_distance_prepare(input, kernel)?;
562 let warm = (first + warmup_period(params)).min(data.len());
563 let mut oscillator = alloc_with_nan_prefix(data.len(), warm);
564 let mut signal = alloc_with_nan_prefix(data.len(), warm);
565 stochastic_distance_row_from_slice(data, params, &mut oscillator, &mut signal);
566 Ok(StochasticDistanceOutput { oscillator, signal })
567}
568
569pub fn stochastic_distance_into_slices(
570 oscillator_out: &mut [f64],
571 signal_out: &mut [f64],
572 input: &StochasticDistanceInput,
573 kernel: Kernel,
574) -> Result<(), StochasticDistanceError> {
575 let expected = input.as_ref().len();
576 if oscillator_out.len() != expected || signal_out.len() != expected {
577 return Err(StochasticDistanceError::OutputLengthMismatch {
578 expected,
579 oscillator_got: oscillator_out.len(),
580 signal_got: signal_out.len(),
581 });
582 }
583 let (data, _first, _valid, params, _chosen) = stochastic_distance_prepare(input, kernel)?;
584 stochastic_distance_row_from_slice(data, params, oscillator_out, signal_out);
585 Ok(())
586}
587
588#[derive(Debug, Clone)]
589#[cfg_attr(
590 all(target_arch = "wasm32", feature = "wasm"),
591 derive(Serialize, Deserialize)
592)]
593pub struct StochasticDistanceBatchRange {
594 pub lookback_length: (usize, usize, usize),
595 pub length1: (usize, usize, usize),
596 pub length2: (usize, usize, usize),
597 pub ob_level: (i32, i32, i32),
598 pub os_level: (i32, i32, i32),
599}
600
601impl Default for StochasticDistanceBatchRange {
602 fn default() -> Self {
603 Self {
604 lookback_length: (DEFAULT_LOOKBACK_LENGTH, DEFAULT_LOOKBACK_LENGTH, 0),
605 length1: (DEFAULT_LENGTH1, DEFAULT_LENGTH1, 0),
606 length2: (DEFAULT_LENGTH2, DEFAULT_LENGTH2, 0),
607 ob_level: (DEFAULT_OB_LEVEL, DEFAULT_OB_LEVEL, 0),
608 os_level: (DEFAULT_OS_LEVEL, DEFAULT_OS_LEVEL, 0),
609 }
610 }
611}
612
613#[derive(Debug, Clone)]
614pub struct StochasticDistanceBatchOutput {
615 pub oscillator: Vec<f64>,
616 pub signal: Vec<f64>,
617 pub combos: Vec<StochasticDistanceParams>,
618 pub rows: usize,
619 pub cols: usize,
620}
621
622#[derive(Clone, Debug, Default)]
623pub struct StochasticDistanceBatchBuilder {
624 sweep: StochasticDistanceBatchRange,
625 kernel: Kernel,
626}
627
628impl StochasticDistanceBatchBuilder {
629 #[inline]
630 pub fn new() -> Self {
631 Self::default()
632 }
633
634 #[inline]
635 pub fn lookback_length(mut self, start: usize, end: usize, step: usize) -> Self {
636 self.sweep.lookback_length = (start, end, step);
637 self
638 }
639
640 #[inline]
641 pub fn length1(mut self, start: usize, end: usize, step: usize) -> Self {
642 self.sweep.length1 = (start, end, step);
643 self
644 }
645
646 #[inline]
647 pub fn length2(mut self, start: usize, end: usize, step: usize) -> Self {
648 self.sweep.length2 = (start, end, step);
649 self
650 }
651
652 #[inline]
653 pub fn ob_level(mut self, start: i32, end: i32, step: i32) -> Self {
654 self.sweep.ob_level = (start, end, step);
655 self
656 }
657
658 #[inline]
659 pub fn os_level(mut self, start: i32, end: i32, step: i32) -> Self {
660 self.sweep.os_level = (start, end, step);
661 self
662 }
663
664 #[inline]
665 pub fn kernel(mut self, kernel: Kernel) -> Self {
666 self.kernel = kernel;
667 self
668 }
669
670 #[inline]
671 pub fn apply_slice(
672 self,
673 data: &[f64],
674 ) -> Result<StochasticDistanceBatchOutput, StochasticDistanceError> {
675 stochastic_distance_batch_with_kernel(data, &self.sweep, self.kernel)
676 }
677}
678
679#[inline]
680fn expand_axis_usize(
681 start: usize,
682 end: usize,
683 step: usize,
684) -> Result<Vec<usize>, StochasticDistanceError> {
685 if start > end {
686 return Err(StochasticDistanceError::InvalidRange {
687 start: start.to_string(),
688 end: end.to_string(),
689 step: step.to_string(),
690 });
691 }
692 if start == end {
693 if step != 0 {
694 return Err(StochasticDistanceError::InvalidRange {
695 start: start.to_string(),
696 end: end.to_string(),
697 step: step.to_string(),
698 });
699 }
700 return Ok(vec![start]);
701 }
702 if step == 0 {
703 return Err(StochasticDistanceError::InvalidRange {
704 start: start.to_string(),
705 end: end.to_string(),
706 step: step.to_string(),
707 });
708 }
709 let mut out = Vec::new();
710 let mut value = start;
711 while value <= end {
712 out.push(value);
713 match value.checked_add(step) {
714 Some(next) => value = next,
715 None => break,
716 }
717 }
718 if *out.last().unwrap_or(&start) != end {
719 return Err(StochasticDistanceError::InvalidRange {
720 start: start.to_string(),
721 end: end.to_string(),
722 step: step.to_string(),
723 });
724 }
725 Ok(out)
726}
727
728#[inline]
729fn expand_axis_i32(start: i32, end: i32, step: i32) -> Result<Vec<i32>, StochasticDistanceError> {
730 if start > end {
731 return Err(StochasticDistanceError::InvalidRange {
732 start: start.to_string(),
733 end: end.to_string(),
734 step: step.to_string(),
735 });
736 }
737 if start == end {
738 if step != 0 {
739 return Err(StochasticDistanceError::InvalidRange {
740 start: start.to_string(),
741 end: end.to_string(),
742 step: step.to_string(),
743 });
744 }
745 return Ok(vec![start]);
746 }
747 if step <= 0 {
748 return Err(StochasticDistanceError::InvalidRange {
749 start: start.to_string(),
750 end: end.to_string(),
751 step: step.to_string(),
752 });
753 }
754 let mut out = Vec::new();
755 let mut value = start;
756 while value <= end {
757 out.push(value);
758 match value.checked_add(step) {
759 Some(next) => value = next,
760 None => break,
761 }
762 }
763 if *out.last().unwrap_or(&start) != end {
764 return Err(StochasticDistanceError::InvalidRange {
765 start: start.to_string(),
766 end: end.to_string(),
767 step: step.to_string(),
768 });
769 }
770 Ok(out)
771}
772
773#[inline]
774fn expand_grid_stochastic_distance(
775 range: &StochasticDistanceBatchRange,
776) -> Result<Vec<StochasticDistanceParams>, StochasticDistanceError> {
777 let lookbacks = expand_axis_usize(
778 range.lookback_length.0,
779 range.lookback_length.1,
780 range.lookback_length.2,
781 )?;
782 let length1s = expand_axis_usize(range.length1.0, range.length1.1, range.length1.2)?;
783 let length2s = expand_axis_usize(range.length2.0, range.length2.1, range.length2.2)?;
784 let ob_levels = expand_axis_i32(range.ob_level.0, range.ob_level.1, range.ob_level.2)?;
785 let os_levels = expand_axis_i32(range.os_level.0, range.os_level.1, range.os_level.2)?;
786
787 let mut combos = Vec::with_capacity(
788 lookbacks.len() * length1s.len() * length2s.len() * ob_levels.len() * os_levels.len(),
789 );
790 for &lookback_length in &lookbacks {
791 for &length1 in &length1s {
792 for &length2 in &length2s {
793 for &ob_level in &ob_levels {
794 for &os_level in &os_levels {
795 let combo = StochasticDistanceParams {
796 lookback_length: Some(lookback_length),
797 length1: Some(length1),
798 length2: Some(length2),
799 ob_level: Some(ob_level),
800 os_level: Some(os_level),
801 };
802 let _ = resolve_params(&combo, None)?;
803 combos.push(combo);
804 }
805 }
806 }
807 }
808 }
809 Ok(combos)
810}
811
812#[inline]
813pub fn stochastic_distance_batch_with_kernel(
814 data: &[f64],
815 sweep: &StochasticDistanceBatchRange,
816 kernel: Kernel,
817) -> Result<StochasticDistanceBatchOutput, StochasticDistanceError> {
818 let batch_kernel = match kernel {
819 Kernel::Auto => detect_best_batch_kernel(),
820 other if other.is_batch() => other,
821 other => return Err(StochasticDistanceError::InvalidKernelForBatch(other)),
822 };
823 stochastic_distance_batch_par_slice(data, sweep, batch_kernel.to_non_batch())
824}
825
826#[inline]
827pub fn stochastic_distance_batch_slice(
828 data: &[f64],
829 sweep: &StochasticDistanceBatchRange,
830 kernel: Kernel,
831) -> Result<StochasticDistanceBatchOutput, StochasticDistanceError> {
832 stochastic_distance_batch_inner(data, sweep, kernel, false)
833}
834
835#[inline]
836pub fn stochastic_distance_batch_par_slice(
837 data: &[f64],
838 sweep: &StochasticDistanceBatchRange,
839 kernel: Kernel,
840) -> Result<StochasticDistanceBatchOutput, StochasticDistanceError> {
841 stochastic_distance_batch_inner(data, sweep, kernel, true)
842}
843
844pub fn stochastic_distance_batch_inner(
845 data: &[f64],
846 sweep: &StochasticDistanceBatchRange,
847 _kernel: Kernel,
848 parallel: bool,
849) -> Result<StochasticDistanceBatchOutput, StochasticDistanceError> {
850 let combos = expand_grid_stochastic_distance(sweep)?;
851 let rows = combos.len();
852 let cols = data.len();
853 if cols == 0 {
854 return Err(StochasticDistanceError::EmptyInputData);
855 }
856
857 let first = first_valid_value(data);
858 if first >= cols {
859 return Err(StochasticDistanceError::AllValuesNaN);
860 }
861 let valid = count_valid_values(data);
862 let max_needed = combos
863 .iter()
864 .map(|combo| {
865 combo.lookback_length.unwrap_or(DEFAULT_LOOKBACK_LENGTH)
866 + combo.length1.unwrap_or(DEFAULT_LENGTH1)
867 })
868 .max()
869 .unwrap_or(0);
870 if valid < max_needed {
871 return Err(StochasticDistanceError::NotEnoughValidData {
872 needed: max_needed,
873 valid,
874 });
875 }
876
877 let mut oscillator_mu = make_uninit_matrix(rows, cols);
878 let mut signal_mu = make_uninit_matrix(rows, cols);
879 let warmups: Vec<usize> = combos
880 .iter()
881 .map(|combo| {
882 first
883 + combo.length1.unwrap_or(DEFAULT_LENGTH1)
884 + combo.lookback_length.unwrap_or(DEFAULT_LOOKBACK_LENGTH)
885 - 1
886 })
887 .collect();
888 init_matrix_prefixes(&mut oscillator_mu, cols, &warmups);
889 init_matrix_prefixes(&mut signal_mu, cols, &warmups);
890
891 let mut oscillator_guard = ManuallyDrop::new(oscillator_mu);
892 let mut signal_guard = ManuallyDrop::new(signal_mu);
893 let oscillator_out: &mut [f64] = unsafe {
894 std::slice::from_raw_parts_mut(
895 oscillator_guard.as_mut_ptr() as *mut f64,
896 oscillator_guard.len(),
897 )
898 };
899 let signal_out: &mut [f64] = unsafe {
900 std::slice::from_raw_parts_mut(signal_guard.as_mut_ptr() as *mut f64, signal_guard.len())
901 };
902
903 if parallel {
904 #[cfg(not(target_arch = "wasm32"))]
905 oscillator_out
906 .par_chunks_mut(cols)
907 .zip(signal_out.par_chunks_mut(cols))
908 .enumerate()
909 .for_each(|(row, (osc_row, sig_row))| {
910 let params = resolve_params(&combos[row], Some(cols)).unwrap();
911 stochastic_distance_row_from_slice(data, params, osc_row, sig_row);
912 });
913
914 #[cfg(target_arch = "wasm32")]
915 for (row, (osc_row, sig_row)) in oscillator_out
916 .chunks_mut(cols)
917 .zip(signal_out.chunks_mut(cols))
918 .enumerate()
919 {
920 let params = resolve_params(&combos[row], Some(cols)).unwrap();
921 stochastic_distance_row_from_slice(data, params, osc_row, sig_row);
922 }
923 } else {
924 for (row, (osc_row, sig_row)) in oscillator_out
925 .chunks_mut(cols)
926 .zip(signal_out.chunks_mut(cols))
927 .enumerate()
928 {
929 let params = resolve_params(&combos[row], Some(cols)).unwrap();
930 stochastic_distance_row_from_slice(data, params, osc_row, sig_row);
931 }
932 }
933
934 let oscillator = unsafe {
935 Vec::from_raw_parts(
936 oscillator_guard.as_mut_ptr() as *mut f64,
937 oscillator_guard.len(),
938 oscillator_guard.capacity(),
939 )
940 };
941 let signal = unsafe {
942 Vec::from_raw_parts(
943 signal_guard.as_mut_ptr() as *mut f64,
944 signal_guard.len(),
945 signal_guard.capacity(),
946 )
947 };
948
949 Ok(StochasticDistanceBatchOutput {
950 oscillator,
951 signal,
952 combos,
953 rows,
954 cols,
955 })
956}
957
958pub fn stochastic_distance_batch_inner_into(
959 data: &[f64],
960 sweep: &StochasticDistanceBatchRange,
961 _kernel: Kernel,
962 parallel: bool,
963 oscillator_out: &mut [f64],
964 signal_out: &mut [f64],
965) -> Result<Vec<StochasticDistanceParams>, StochasticDistanceError> {
966 let combos = expand_grid_stochastic_distance(sweep)?;
967 let rows = combos.len();
968 let cols = data.len();
969 if cols == 0 {
970 return Err(StochasticDistanceError::EmptyInputData);
971 }
972 let total = rows
973 .checked_mul(cols)
974 .ok_or(StochasticDistanceError::OutputLengthMismatch {
975 expected: usize::MAX,
976 oscillator_got: oscillator_out.len(),
977 signal_got: signal_out.len(),
978 })?;
979 if oscillator_out.len() != total || signal_out.len() != total {
980 return Err(StochasticDistanceError::OutputLengthMismatch {
981 expected: total,
982 oscillator_got: oscillator_out.len(),
983 signal_got: signal_out.len(),
984 });
985 }
986
987 let first = first_valid_value(data);
988 if first >= cols {
989 return Err(StochasticDistanceError::AllValuesNaN);
990 }
991 let valid = count_valid_values(data);
992 let max_needed = combos
993 .iter()
994 .map(|combo| {
995 combo.lookback_length.unwrap_or(DEFAULT_LOOKBACK_LENGTH)
996 + combo.length1.unwrap_or(DEFAULT_LENGTH1)
997 })
998 .max()
999 .unwrap_or(0);
1000 if valid < max_needed {
1001 return Err(StochasticDistanceError::NotEnoughValidData {
1002 needed: max_needed,
1003 valid,
1004 });
1005 }
1006
1007 if parallel {
1008 #[cfg(not(target_arch = "wasm32"))]
1009 oscillator_out
1010 .par_chunks_mut(cols)
1011 .zip(signal_out.par_chunks_mut(cols))
1012 .enumerate()
1013 .for_each(|(row, (osc_row, sig_row))| {
1014 let params = resolve_params(&combos[row], Some(cols)).unwrap();
1015 stochastic_distance_row_from_slice(data, params, osc_row, sig_row);
1016 });
1017
1018 #[cfg(target_arch = "wasm32")]
1019 for (row, (osc_row, sig_row)) in oscillator_out
1020 .chunks_mut(cols)
1021 .zip(signal_out.chunks_mut(cols))
1022 .enumerate()
1023 {
1024 let params = resolve_params(&combos[row], Some(cols)).unwrap();
1025 stochastic_distance_row_from_slice(data, params, osc_row, sig_row);
1026 }
1027 } else {
1028 for (row, (osc_row, sig_row)) in oscillator_out
1029 .chunks_mut(cols)
1030 .zip(signal_out.chunks_mut(cols))
1031 .enumerate()
1032 {
1033 let params = resolve_params(&combos[row], Some(cols)).unwrap();
1034 stochastic_distance_row_from_slice(data, params, osc_row, sig_row);
1035 }
1036 }
1037
1038 Ok(combos)
1039}
1040
1041#[cfg(feature = "python")]
1042#[pyfunction(name = "stochastic_distance")]
1043#[pyo3(signature = (data, lookback_length=DEFAULT_LOOKBACK_LENGTH, length1=DEFAULT_LENGTH1, length2=DEFAULT_LENGTH2, ob_level=DEFAULT_OB_LEVEL, os_level=DEFAULT_OS_LEVEL, kernel=None))]
1044pub fn stochastic_distance_py<'py>(
1045 py: Python<'py>,
1046 data: PyReadonlyArray1<'py, f64>,
1047 lookback_length: usize,
1048 length1: usize,
1049 length2: usize,
1050 ob_level: i32,
1051 os_level: i32,
1052 kernel: Option<&str>,
1053) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1054 let data = data.as_slice()?;
1055 let kernel = validate_kernel(kernel, false)?;
1056 let input = StochasticDistanceInput::from_slice(
1057 data,
1058 StochasticDistanceParams {
1059 lookback_length: Some(lookback_length),
1060 length1: Some(length1),
1061 length2: Some(length2),
1062 ob_level: Some(ob_level),
1063 os_level: Some(os_level),
1064 },
1065 );
1066 let output = py
1067 .allow_threads(|| stochastic_distance_with_kernel(&input, kernel))
1068 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1069 Ok((
1070 output.oscillator.into_pyarray(py),
1071 output.signal.into_pyarray(py),
1072 ))
1073}
1074
1075#[cfg(feature = "python")]
1076#[pyclass(name = "StochasticDistanceStream")]
1077pub struct StochasticDistanceStreamPy {
1078 stream: StochasticDistanceStream,
1079}
1080
1081#[cfg(feature = "python")]
1082#[pymethods]
1083impl StochasticDistanceStreamPy {
1084 #[new]
1085 #[pyo3(signature = (lookback_length=DEFAULT_LOOKBACK_LENGTH, length1=DEFAULT_LENGTH1, length2=DEFAULT_LENGTH2, ob_level=DEFAULT_OB_LEVEL, os_level=DEFAULT_OS_LEVEL))]
1086 fn new(
1087 lookback_length: usize,
1088 length1: usize,
1089 length2: usize,
1090 ob_level: i32,
1091 os_level: i32,
1092 ) -> PyResult<Self> {
1093 let stream = StochasticDistanceStream::try_new(StochasticDistanceParams {
1094 lookback_length: Some(lookback_length),
1095 length1: Some(length1),
1096 length2: Some(length2),
1097 ob_level: Some(ob_level),
1098 os_level: Some(os_level),
1099 })
1100 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1101 Ok(Self { stream })
1102 }
1103
1104 fn update(&mut self, value: f64) -> Option<(f64, f64)> {
1105 self.stream.update(value)
1106 }
1107
1108 #[getter]
1109 fn warmup_period(&self) -> usize {
1110 self.stream.get_warmup_period()
1111 }
1112}
1113
1114#[cfg(feature = "python")]
1115#[pyfunction(name = "stochastic_distance_batch")]
1116#[pyo3(signature = (data, lookback_length_range=(DEFAULT_LOOKBACK_LENGTH, DEFAULT_LOOKBACK_LENGTH, 0), length1_range=(DEFAULT_LENGTH1, DEFAULT_LENGTH1, 0), length2_range=(DEFAULT_LENGTH2, DEFAULT_LENGTH2, 0), ob_level_range=(DEFAULT_OB_LEVEL, DEFAULT_OB_LEVEL, 0), os_level_range=(DEFAULT_OS_LEVEL, DEFAULT_OS_LEVEL, 0), kernel=None))]
1117pub fn stochastic_distance_batch_py<'py>(
1118 py: Python<'py>,
1119 data: PyReadonlyArray1<'py, f64>,
1120 lookback_length_range: (usize, usize, usize),
1121 length1_range: (usize, usize, usize),
1122 length2_range: (usize, usize, usize),
1123 ob_level_range: (i32, i32, i32),
1124 os_level_range: (i32, i32, i32),
1125 kernel: Option<&str>,
1126) -> PyResult<Bound<'py, PyDict>> {
1127 let data = data.as_slice()?;
1128 let kernel = validate_kernel(kernel, true)?;
1129 let sweep = StochasticDistanceBatchRange {
1130 lookback_length: lookback_length_range,
1131 length1: length1_range,
1132 length2: length2_range,
1133 ob_level: ob_level_range,
1134 os_level: os_level_range,
1135 };
1136 let combos = expand_grid_stochastic_distance(&sweep)
1137 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1138 let rows = combos.len();
1139 let cols = data.len();
1140 let total = rows
1141 .checked_mul(cols)
1142 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1143
1144 let oscillator_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1145 let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1146 let oscillator_slice = unsafe { oscillator_arr.as_slice_mut()? };
1147 let signal_slice = unsafe { signal_arr.as_slice_mut()? };
1148
1149 let combos = py
1150 .allow_threads(|| {
1151 let batch = match kernel {
1152 Kernel::Auto => detect_best_batch_kernel(),
1153 other => other,
1154 };
1155 stochastic_distance_batch_inner_into(
1156 data,
1157 &sweep,
1158 batch.to_non_batch(),
1159 true,
1160 oscillator_slice,
1161 signal_slice,
1162 )
1163 })
1164 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1165
1166 let dict = PyDict::new(py);
1167 dict.set_item("oscillator", oscillator_arr.reshape((rows, cols))?)?;
1168 dict.set_item("signal", signal_arr.reshape((rows, cols))?)?;
1169 dict.set_item(
1170 "lookback_lengths",
1171 combos
1172 .iter()
1173 .map(|combo| combo.lookback_length.unwrap_or(DEFAULT_LOOKBACK_LENGTH) as u64)
1174 .collect::<Vec<_>>()
1175 .into_pyarray(py),
1176 )?;
1177 dict.set_item(
1178 "length1s",
1179 combos
1180 .iter()
1181 .map(|combo| combo.length1.unwrap_or(DEFAULT_LENGTH1) as u64)
1182 .collect::<Vec<_>>()
1183 .into_pyarray(py),
1184 )?;
1185 dict.set_item(
1186 "length2s",
1187 combos
1188 .iter()
1189 .map(|combo| combo.length2.unwrap_or(DEFAULT_LENGTH2) as u64)
1190 .collect::<Vec<_>>()
1191 .into_pyarray(py),
1192 )?;
1193 dict.set_item(
1194 "ob_levels",
1195 combos
1196 .iter()
1197 .map(|combo| combo.ob_level.unwrap_or(DEFAULT_OB_LEVEL))
1198 .collect::<Vec<_>>()
1199 .into_pyarray(py),
1200 )?;
1201 dict.set_item(
1202 "os_levels",
1203 combos
1204 .iter()
1205 .map(|combo| combo.os_level.unwrap_or(DEFAULT_OS_LEVEL))
1206 .collect::<Vec<_>>()
1207 .into_pyarray(py),
1208 )?;
1209 dict.set_item("rows", rows)?;
1210 dict.set_item("cols", cols)?;
1211 Ok(dict)
1212}
1213
1214#[cfg(feature = "python")]
1215pub fn register_stochastic_distance_module(
1216 module: &Bound<'_, pyo3::types::PyModule>,
1217) -> PyResult<()> {
1218 module.add_function(wrap_pyfunction!(stochastic_distance_py, module)?)?;
1219 module.add_function(wrap_pyfunction!(stochastic_distance_batch_py, module)?)?;
1220 module.add_class::<StochasticDistanceStreamPy>()?;
1221 Ok(())
1222}
1223
1224#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1225#[wasm_bindgen(js_name = "stochastic_distance_js")]
1226pub fn stochastic_distance_js(
1227 data: &[f64],
1228 lookback_length: usize,
1229 length1: usize,
1230 length2: usize,
1231 ob_level: i32,
1232 os_level: i32,
1233) -> Result<JsValue, JsValue> {
1234 let input = StochasticDistanceInput::from_slice(
1235 data,
1236 StochasticDistanceParams {
1237 lookback_length: Some(lookback_length),
1238 length1: Some(length1),
1239 length2: Some(length2),
1240 ob_level: Some(ob_level),
1241 os_level: Some(os_level),
1242 },
1243 );
1244 let out = stochastic_distance(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1245 let result = js_sys::Object::new();
1246
1247 let oscillator = js_sys::Float64Array::new_with_length(out.oscillator.len() as u32);
1248 oscillator.copy_from(&out.oscillator);
1249 js_sys::Reflect::set(&result, &JsValue::from_str("oscillator"), &oscillator)?;
1250
1251 let signal = js_sys::Float64Array::new_with_length(out.signal.len() as u32);
1252 signal.copy_from(&out.signal);
1253 js_sys::Reflect::set(&result, &JsValue::from_str("signal"), &signal)?;
1254
1255 Ok(result.into())
1256}
1257
1258#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1259#[wasm_bindgen]
1260pub fn stochastic_distance_alloc(len: usize) -> *mut f64 {
1261 let mut vec = Vec::<f64>::with_capacity(len);
1262 let ptr = vec.as_mut_ptr();
1263 std::mem::forget(vec);
1264 ptr
1265}
1266
1267#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1268#[wasm_bindgen]
1269pub fn stochastic_distance_free(ptr: *mut f64, len: usize) {
1270 if !ptr.is_null() {
1271 unsafe {
1272 let _ = Vec::from_raw_parts(ptr, len, len);
1273 }
1274 }
1275}
1276
1277#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1278#[wasm_bindgen]
1279pub fn stochastic_distance_into(
1280 data_ptr: *const f64,
1281 oscillator_ptr: *mut f64,
1282 signal_ptr: *mut f64,
1283 len: usize,
1284 lookback_length: usize,
1285 length1: usize,
1286 length2: usize,
1287 ob_level: i32,
1288 os_level: i32,
1289) -> Result<(), JsValue> {
1290 if data_ptr.is_null() || oscillator_ptr.is_null() || signal_ptr.is_null() {
1291 return Err(JsValue::from_str("Null pointer provided"));
1292 }
1293 unsafe {
1294 let data = std::slice::from_raw_parts(data_ptr, len);
1295 let input = StochasticDistanceInput::from_slice(
1296 data,
1297 StochasticDistanceParams {
1298 lookback_length: Some(lookback_length),
1299 length1: Some(length1),
1300 length2: Some(length2),
1301 ob_level: Some(ob_level),
1302 os_level: Some(os_level),
1303 },
1304 );
1305 let alias = data_ptr == oscillator_ptr || data_ptr == signal_ptr;
1306 if alias {
1307 let mut oscillator_tmp = vec![0.0; len];
1308 let mut signal_tmp = vec![0.0; len];
1309 stochastic_distance_into_slices(
1310 &mut oscillator_tmp,
1311 &mut signal_tmp,
1312 &input,
1313 Kernel::Auto,
1314 )
1315 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1316 std::slice::from_raw_parts_mut(oscillator_ptr, len).copy_from_slice(&oscillator_tmp);
1317 std::slice::from_raw_parts_mut(signal_ptr, len).copy_from_slice(&signal_tmp);
1318 } else {
1319 let oscillator_out = std::slice::from_raw_parts_mut(oscillator_ptr, len);
1320 let signal_out = std::slice::from_raw_parts_mut(signal_ptr, len);
1321 stochastic_distance_into_slices(oscillator_out, signal_out, &input, Kernel::Auto)
1322 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1323 }
1324 }
1325 Ok(())
1326}
1327
1328#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1329#[derive(Serialize, Deserialize)]
1330pub struct StochasticDistanceBatchConfig {
1331 pub lookback_length_range: (usize, usize, usize),
1332 pub length1_range: (usize, usize, usize),
1333 pub length2_range: (usize, usize, usize),
1334 pub ob_level_range: (i32, i32, i32),
1335 pub os_level_range: (i32, i32, i32),
1336}
1337
1338#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1339#[derive(Serialize, Deserialize)]
1340pub struct StochasticDistanceBatchJsOutput {
1341 pub oscillator: Vec<f64>,
1342 pub signal: Vec<f64>,
1343 pub combos: Vec<StochasticDistanceParams>,
1344 pub lookback_lengths: Vec<usize>,
1345 pub length1s: Vec<usize>,
1346 pub length2s: Vec<usize>,
1347 pub ob_levels: Vec<i32>,
1348 pub os_levels: Vec<i32>,
1349 pub rows: usize,
1350 pub cols: usize,
1351}
1352
1353#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1354#[wasm_bindgen(js_name = "stochastic_distance_batch_js")]
1355pub fn stochastic_distance_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1356 let config: StochasticDistanceBatchConfig = serde_wasm_bindgen::from_value(config)
1357 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1358 let sweep = StochasticDistanceBatchRange {
1359 lookback_length: config.lookback_length_range,
1360 length1: config.length1_range,
1361 length2: config.length2_range,
1362 ob_level: config.ob_level_range,
1363 os_level: config.os_level_range,
1364 };
1365 let output = stochastic_distance_batch_inner(data, &sweep, detect_best_kernel(), false)
1366 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1367 serde_wasm_bindgen::to_value(&StochasticDistanceBatchJsOutput {
1368 lookback_lengths: output
1369 .combos
1370 .iter()
1371 .map(|combo| combo.lookback_length.unwrap_or(DEFAULT_LOOKBACK_LENGTH))
1372 .collect(),
1373 length1s: output
1374 .combos
1375 .iter()
1376 .map(|combo| combo.length1.unwrap_or(DEFAULT_LENGTH1))
1377 .collect(),
1378 length2s: output
1379 .combos
1380 .iter()
1381 .map(|combo| combo.length2.unwrap_or(DEFAULT_LENGTH2))
1382 .collect(),
1383 ob_levels: output
1384 .combos
1385 .iter()
1386 .map(|combo| combo.ob_level.unwrap_or(DEFAULT_OB_LEVEL))
1387 .collect(),
1388 os_levels: output
1389 .combos
1390 .iter()
1391 .map(|combo| combo.os_level.unwrap_or(DEFAULT_OS_LEVEL))
1392 .collect(),
1393 oscillator: output.oscillator,
1394 signal: output.signal,
1395 combos: output.combos,
1396 rows: output.rows,
1397 cols: output.cols,
1398 })
1399 .map_err(|e| JsValue::from_str(&e.to_string()))
1400}
1401
1402#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1403#[wasm_bindgen]
1404pub fn stochastic_distance_batch_into(
1405 data_ptr: *const f64,
1406 oscillator_ptr: *mut f64,
1407 signal_ptr: *mut f64,
1408 len: usize,
1409 lookback_length_start: usize,
1410 lookback_length_end: usize,
1411 lookback_length_step: usize,
1412 length1_start: usize,
1413 length1_end: usize,
1414 length1_step: usize,
1415 length2_start: usize,
1416 length2_end: usize,
1417 length2_step: usize,
1418 ob_level_start: i32,
1419 ob_level_end: i32,
1420 ob_level_step: i32,
1421 os_level_start: i32,
1422 os_level_end: i32,
1423 os_level_step: i32,
1424) -> Result<usize, JsValue> {
1425 if data_ptr.is_null() || oscillator_ptr.is_null() || signal_ptr.is_null() {
1426 return Err(JsValue::from_str("Null pointer provided"));
1427 }
1428 let sweep = StochasticDistanceBatchRange {
1429 lookback_length: (
1430 lookback_length_start,
1431 lookback_length_end,
1432 lookback_length_step,
1433 ),
1434 length1: (length1_start, length1_end, length1_step),
1435 length2: (length2_start, length2_end, length2_step),
1436 ob_level: (ob_level_start, ob_level_end, ob_level_step),
1437 os_level: (os_level_start, os_level_end, os_level_step),
1438 };
1439 let combos =
1440 expand_grid_stochastic_distance(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1441 let rows = combos.len();
1442 unsafe {
1443 let data = std::slice::from_raw_parts(data_ptr, len);
1444 let total = rows
1445 .checked_mul(len)
1446 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1447 let oscillator_out = std::slice::from_raw_parts_mut(oscillator_ptr, total);
1448 let signal_out = std::slice::from_raw_parts_mut(signal_ptr, total);
1449 stochastic_distance_batch_inner_into(
1450 data,
1451 &sweep,
1452 detect_best_kernel(),
1453 false,
1454 oscillator_out,
1455 signal_out,
1456 )
1457 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1458 }
1459 Ok(rows)
1460}
1461
1462#[cfg(test)]
1463mod tests {
1464 use super::*;
1465
1466 fn sample_close(length: usize) -> Vec<f64> {
1467 let mut out = vec![f64::NAN; length];
1468 let mut prev = 100.0;
1469 for (i, slot) in out.iter_mut().enumerate().skip(2) {
1470 let x = i as f64;
1471 let value = prev + x.sin() * 0.75 + (x * 0.11).cos() * 1.25 + x * 0.03;
1472 *slot = value;
1473 prev = value;
1474 }
1475 out
1476 }
1477
1478 #[test]
1479 fn stochastic_distance_output_contract() {
1480 let data = sample_close(512);
1481 let input = StochasticDistanceInput::from_slice(
1482 &data,
1483 StochasticDistanceParams {
1484 lookback_length: Some(80),
1485 length1: Some(12),
1486 length2: Some(3),
1487 ob_level: Some(40),
1488 os_level: Some(-40),
1489 },
1490 );
1491 let out = stochastic_distance(&input).unwrap();
1492
1493 assert_eq!(out.oscillator.len(), data.len());
1494 assert_eq!(out.signal.len(), data.len());
1495 let first_valid = out.oscillator.iter().position(|v| v.is_finite()).unwrap();
1496 assert!(first_valid >= 91);
1497 for &v in out.signal.iter().skip(first_valid + 16) {
1498 assert!(v.is_nan() || v == -1.0 || v == 0.0 || v == 1.0);
1499 }
1500 }
1501
1502 #[test]
1503 fn stochastic_distance_rejects_invalid_parameters() {
1504 let data = sample_close(64);
1505
1506 let err = stochastic_distance(&StochasticDistanceInput::from_slice(
1507 &data,
1508 StochasticDistanceParams {
1509 lookback_length: Some(0),
1510 ..StochasticDistanceParams::default()
1511 },
1512 ))
1513 .unwrap_err();
1514 assert!(matches!(
1515 err,
1516 StochasticDistanceError::InvalidLookbackLength { .. }
1517 ));
1518
1519 let err = stochastic_distance(&StochasticDistanceInput::from_slice(
1520 &data,
1521 StochasticDistanceParams {
1522 os_level: Some(10),
1523 ..StochasticDistanceParams::default()
1524 },
1525 ))
1526 .unwrap_err();
1527 assert!(matches!(
1528 err,
1529 StochasticDistanceError::InvalidOsLevel { .. }
1530 ));
1531 }
1532
1533 #[test]
1534 fn stochastic_distance_stream_matches_batch_with_reset() {
1535 let mut data = sample_close(256);
1536 data[120] = f64::NAN;
1537
1538 let params = StochasticDistanceParams {
1539 lookback_length: Some(60),
1540 length1: Some(10),
1541 length2: Some(4),
1542 ob_level: Some(35),
1543 os_level: Some(-35),
1544 };
1545 let batch =
1546 stochastic_distance(&StochasticDistanceInput::from_slice(&data, params.clone()))
1547 .unwrap();
1548 let mut stream = StochasticDistanceStream::try_new(params).unwrap();
1549
1550 let mut osc = Vec::with_capacity(data.len());
1551 let mut sig = Vec::with_capacity(data.len());
1552 for &value in &data {
1553 match stream.update(value) {
1554 Some((o, s)) => {
1555 osc.push(o);
1556 sig.push(s);
1557 }
1558 None => {
1559 osc.push(f64::NAN);
1560 sig.push(f64::NAN);
1561 }
1562 }
1563 }
1564
1565 for i in 0..osc.len() {
1566 let a = osc[i];
1567 let b = batch.oscillator[i];
1568 assert!(
1569 a.is_nan() && b.is_nan() || (a - b).abs() <= 1e-12,
1570 "osc mismatch at {i}"
1571 );
1572 let sa = sig[i];
1573 let sb = batch.signal[i];
1574 assert!(
1575 sa.is_nan() && sb.is_nan() || (sa - sb).abs() <= 1e-12,
1576 "signal mismatch at {i}"
1577 );
1578 }
1579 }
1580
1581 #[test]
1582 fn stochastic_distance_batch_single_param_matches_single() {
1583 let data = sample_close(192);
1584 let sweep = StochasticDistanceBatchRange {
1585 lookback_length: (50, 50, 0),
1586 length1: (8, 8, 0),
1587 length2: (4, 4, 0),
1588 ob_level: (40, 40, 0),
1589 os_level: (-40, -40, 0),
1590 };
1591 let batch = stochastic_distance_batch_with_kernel(&data, &sweep, Kernel::Auto).unwrap();
1592 let direct = stochastic_distance(&StochasticDistanceInput::from_slice(
1593 &data,
1594 StochasticDistanceParams {
1595 lookback_length: Some(50),
1596 length1: Some(8),
1597 length2: Some(4),
1598 ob_level: Some(40),
1599 os_level: Some(-40),
1600 },
1601 ))
1602 .unwrap();
1603
1604 assert_eq!(batch.rows, 1);
1605 assert_eq!(batch.cols, data.len());
1606 for i in 0..data.len() {
1607 let a = batch.oscillator[i];
1608 let b = direct.oscillator[i];
1609 assert!(
1610 a.is_nan() && b.is_nan() || (a - b).abs() <= 1e-12,
1611 "osc mismatch at {i}"
1612 );
1613 let sa = batch.signal[i];
1614 let sb = direct.signal[i];
1615 assert!(
1616 sa.is_nan() && sb.is_nan() || (sa - sb).abs() <= 1e-12,
1617 "signal mismatch at {i}"
1618 );
1619 }
1620 }
1621}