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