Skip to main content

vector_ta/indicators/
stochastic_adaptive_d.rs

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}