Skip to main content

vector_ta/indicators/
adosc.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1};
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::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;
23use aligned_vec::{AVec, CACHELINE_ALIGN};
24#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
25use core::arch::x86_64::*;
26#[cfg(not(target_arch = "wasm32"))]
27use rayon::prelude::*;
28use thiserror::Error;
29
30#[cfg(all(feature = "python", feature = "cuda"))]
31use crate::cuda::CudaAdosc;
32
33#[derive(Debug, Clone)]
34pub enum AdoscData<'a> {
35    Candles {
36        candles: &'a Candles,
37    },
38    Slices {
39        high: &'a [f64],
40        low: &'a [f64],
41        close: &'a [f64],
42        volume: &'a [f64],
43    },
44}
45
46#[derive(Debug, Clone)]
47pub struct AdoscOutput {
48    pub values: Vec<f64>,
49}
50
51#[derive(Debug, Clone)]
52#[cfg_attr(
53    all(target_arch = "wasm32", feature = "wasm"),
54    derive(Serialize, Deserialize)
55)]
56pub struct AdoscParams {
57    pub short_period: Option<usize>,
58    pub long_period: Option<usize>,
59}
60
61impl Default for AdoscParams {
62    fn default() -> Self {
63        Self {
64            short_period: Some(3),
65            long_period: Some(10),
66        }
67    }
68}
69
70#[derive(Debug, Clone)]
71pub struct AdoscInput<'a> {
72    pub data: AdoscData<'a>,
73    pub params: AdoscParams,
74}
75
76impl<'a> AdoscInput<'a> {
77    #[inline]
78    pub fn from_candles(candles: &'a Candles, params: AdoscParams) -> Self {
79        Self {
80            data: AdoscData::Candles { candles },
81            params,
82        }
83    }
84    #[inline]
85    pub fn from_slices(
86        high: &'a [f64],
87        low: &'a [f64],
88        close: &'a [f64],
89        volume: &'a [f64],
90        params: AdoscParams,
91    ) -> Self {
92        Self {
93            data: AdoscData::Slices {
94                high,
95                low,
96                close,
97                volume,
98            },
99            params,
100        }
101    }
102    #[inline]
103    pub fn with_default_candles(candles: &'a Candles) -> Self {
104        Self {
105            data: AdoscData::Candles { candles },
106            params: AdoscParams::default(),
107        }
108    }
109    #[inline]
110    pub fn get_short_period(&self) -> usize {
111        self.params.short_period.unwrap_or(3)
112    }
113    #[inline]
114    pub fn get_long_period(&self) -> usize {
115        self.params.long_period.unwrap_or(10)
116    }
117}
118
119#[derive(Copy, Clone, Debug)]
120pub struct AdoscBuilder {
121    short_period: Option<usize>,
122    long_period: Option<usize>,
123    kernel: Kernel,
124}
125
126impl Default for AdoscBuilder {
127    fn default() -> Self {
128        Self {
129            short_period: None,
130            long_period: None,
131            kernel: Kernel::Auto,
132        }
133    }
134}
135
136impl AdoscBuilder {
137    #[inline(always)]
138    pub fn new() -> Self {
139        Self::default()
140    }
141    #[inline(always)]
142    pub fn short_period(mut self, n: usize) -> Self {
143        self.short_period = Some(n);
144        self
145    }
146    #[inline(always)]
147    pub fn long_period(mut self, n: usize) -> Self {
148        self.long_period = Some(n);
149        self
150    }
151    #[inline(always)]
152    pub fn kernel(mut self, k: Kernel) -> Self {
153        self.kernel = k;
154        self
155    }
156    #[inline(always)]
157    pub fn apply(self, c: &Candles) -> Result<AdoscOutput, AdoscError> {
158        let p = AdoscParams {
159            short_period: self.short_period,
160            long_period: self.long_period,
161        };
162        let i = AdoscInput::from_candles(c, p);
163        adosc_with_kernel(&i, self.kernel)
164    }
165    #[inline(always)]
166    pub fn apply_slices(
167        self,
168        high: &[f64],
169        low: &[f64],
170        close: &[f64],
171        volume: &[f64],
172    ) -> Result<AdoscOutput, AdoscError> {
173        let p = AdoscParams {
174            short_period: self.short_period,
175            long_period: self.long_period,
176        };
177        let i = AdoscInput::from_slices(high, low, close, volume, p);
178        adosc_with_kernel(&i, self.kernel)
179    }
180    #[inline(always)]
181    pub fn into_stream(self) -> Result<AdoscStream, AdoscError> {
182        let p = AdoscParams {
183            short_period: self.short_period,
184            long_period: self.long_period,
185        };
186        AdoscStream::try_new(p)
187    }
188}
189
190#[derive(Debug, Error)]
191pub enum AdoscError {
192    #[error("adosc: input data is empty")]
193    EmptyInputData,
194    #[error("adosc: All values are NaN.")]
195    AllValuesNaN,
196    #[error("adosc: Invalid period: short={short}, long={long}, data length={data_len}")]
197    InvalidPeriod {
198        short: usize,
199        long: usize,
200        data_len: usize,
201    },
202    #[error("adosc: short_period must be less than long_period: short={short}, long={long}")]
203    ShortPeriodGreaterThanLong { short: usize, long: usize },
204    #[error("adosc: At least one slice is empty: high={high}, low={low}, close={close}, volume={volume}")]
205    EmptySlices {
206        high: usize,
207        low: usize,
208        close: usize,
209        volume: usize,
210    },
211    #[error("adosc: output length mismatch: expected={expected}, got={got}")]
212    OutputLengthMismatch { expected: usize, got: usize },
213    #[error("adosc: invalid range expansion: start={start}, end={end}, step={step}")]
214    InvalidRange {
215        start: usize,
216        end: usize,
217        step: usize,
218    },
219    #[error("adosc: not enough valid data: needed={needed}, valid={valid}")]
220    NotEnoughValidData { needed: usize, valid: usize },
221    #[error("adosc: invalid kernel for batch path: {0:?}")]
222    InvalidKernelForBatch(crate::utilities::enums::Kernel),
223    #[error("adosc: invalid input: {0}")]
224    InvalidInput(String),
225}
226
227#[inline]
228pub fn adosc(input: &AdoscInput) -> Result<AdoscOutput, AdoscError> {
229    adosc_with_kernel(input, Kernel::Auto)
230}
231
232#[inline(always)]
233fn adosc_prepare<'a>(
234    input: &'a AdoscInput,
235    kernel: Kernel,
236) -> Result<
237    (
238        &'a [f64],
239        &'a [f64],
240        &'a [f64],
241        &'a [f64],
242        usize,
243        usize,
244        usize,
245        usize,
246        Kernel,
247    ),
248    AdoscError,
249> {
250    let (high, low, close, volume) = match &input.data {
251        AdoscData::Candles { candles } => {
252            let n = candles.close.len();
253            if n == 0 {
254                return Err(AdoscError::EmptyInputData);
255            }
256
257            let (hh, ll, cc, vv) = (
258                candles.high.as_slice(),
259                candles.low.as_slice(),
260                candles.close.as_slice(),
261                candles.volume.as_slice(),
262            );
263            let len = cc.len();
264            if hh.len() != len || ll.len() != len || vv.len() != len {
265                return Err(AdoscError::EmptySlices {
266                    high: hh.len(),
267                    low: ll.len(),
268                    close: cc.len(),
269                    volume: vv.len(),
270                });
271            }
272            (hh, ll, cc, vv)
273        }
274        AdoscData::Slices {
275            high,
276            low,
277            close,
278            volume,
279        } => {
280            if high.is_empty() || low.is_empty() || close.is_empty() || volume.is_empty() {
281                if high.is_empty() && low.is_empty() && close.is_empty() && volume.is_empty() {
282                    return Err(AdoscError::EmptyInputData);
283                }
284                return Err(AdoscError::EmptySlices {
285                    high: high.len(),
286                    low: low.len(),
287                    close: close.len(),
288                    volume: volume.len(),
289                });
290            }
291            let len = close.len();
292            if high.len() != len || low.len() != len || volume.len() != len {
293                return Err(AdoscError::EmptySlices {
294                    high: high.len(),
295                    low: low.len(),
296                    close: close.len(),
297                    volume: volume.len(),
298                });
299            }
300            (*high, *low, *close, *volume)
301        }
302    };
303
304    let len = close.len();
305    let short = input.get_short_period();
306    let long = input.get_long_period();
307
308    if short == 0 || long == 0 || long > len {
309        return Err(AdoscError::InvalidPeriod {
310            short,
311            long,
312            data_len: len,
313        });
314    }
315    if short >= long {
316        return Err(AdoscError::ShortPeriodGreaterThanLong { short, long });
317    }
318
319    let all_nan = |s: &[f64]| s.iter().all(|x| x.is_nan());
320    if all_nan(high) && all_nan(low) && all_nan(close) && all_nan(volume) {
321        return Err(AdoscError::AllValuesNaN);
322    }
323
324    let chosen = match kernel {
325        Kernel::Auto => detect_best_kernel(),
326        k => k,
327    };
328    Ok((high, low, close, volume, short, long, 0, len, chosen))
329}
330
331pub fn adosc_with_kernel(input: &AdoscInput, kernel: Kernel) -> Result<AdoscOutput, AdoscError> {
332    let (high, low, close, volume, short, long, first, len, chosen) = adosc_prepare(input, kernel)?;
333
334    match chosen {
335        Kernel::Scalar | Kernel::ScalarBatch => unsafe {
336            adosc_scalar(high, low, close, volume, short, long, first, len)
337        },
338        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
339        Kernel::Avx2 | Kernel::Avx2Batch => unsafe {
340            adosc_avx2(high, low, close, volume, short, long, first, len)
341        },
342        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
343        Kernel::Avx512 | Kernel::Avx512Batch => unsafe {
344            adosc_avx512(high, low, close, volume, short, long, first, len)
345        },
346        #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
347        Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => unsafe {
348            adosc_scalar(high, low, close, volume, short, long, first, len)
349        },
350        _ => unreachable!(),
351    }
352}
353
354#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
355#[inline]
356pub fn adosc_into(input: &AdoscInput, out: &mut [f64]) -> Result<(), AdoscError> {
357    adosc_into_slice(out, input, Kernel::Auto)
358}
359
360#[inline(always)]
361pub unsafe fn adosc_scalar(
362    high: &[f64],
363    low: &[f64],
364    close: &[f64],
365    volume: &[f64],
366    short: usize,
367    long: usize,
368    _first: usize,
369    len: usize,
370) -> Result<AdoscOutput, AdoscError> {
371    debug_assert!(len > 0);
372
373    let alpha_short = 2.0 / (short as f64 + 1.0);
374    let alpha_long = 2.0 / (long as f64 + 1.0);
375    let one_minus_alpha_short = 1.0 - alpha_short;
376    let one_minus_alpha_long = 1.0 - alpha_long;
377
378    let mut out = alloc_with_nan_prefix(len, 0);
379
380    let hp = high.as_ptr();
381    let lp = low.as_ptr();
382    let cp = close.as_ptr();
383    let vp = volume.as_ptr();
384    let op = out.as_mut_ptr();
385
386    let h0 = *hp;
387    let l0 = *lp;
388    let c0 = *cp;
389    let v0 = *vp;
390    let hl0 = h0 - l0;
391    let mfm0 = if hl0 != 0.0 {
392        ((c0 - l0) - (h0 - c0)) / hl0
393    } else {
394        0.0
395    };
396    let mfv0 = mfm0 * v0;
397    let mut sum_ad = mfv0;
398    let mut short_ema = sum_ad;
399    let mut long_ema = sum_ad;
400    *op = short_ema - long_ema;
401
402    let mut i = 1usize;
403    while i < len {
404        let h = *hp.add(i);
405        let l = *lp.add(i);
406        let c = *cp.add(i);
407        let v = *vp.add(i);
408
409        let hl = h - l;
410        let mfm = if hl != 0.0 {
411            ((c - l) - (h - c)) / hl
412        } else {
413            0.0
414        };
415        let mfv = mfm * v;
416        sum_ad += mfv;
417        short_ema = alpha_short * sum_ad + one_minus_alpha_short * short_ema;
418        long_ema = alpha_long * sum_ad + one_minus_alpha_long * long_ema;
419        *op.add(i) = short_ema - long_ema;
420
421        i += 1;
422    }
423
424    Ok(AdoscOutput { values: out })
425}
426
427#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
428#[inline(always)]
429pub unsafe fn adosc_avx2(
430    high: &[f64],
431    low: &[f64],
432    close: &[f64],
433    volume: &[f64],
434    short: usize,
435    long: usize,
436    first: usize,
437    len: usize,
438) -> Result<AdoscOutput, AdoscError> {
439    adosc_scalar(high, low, close, volume, short, long, first, len)
440}
441
442#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
443#[inline(always)]
444pub unsafe fn adosc_avx512(
445    high: &[f64],
446    low: &[f64],
447    close: &[f64],
448    volume: &[f64],
449    short: usize,
450    long: usize,
451    first: usize,
452    len: usize,
453) -> Result<AdoscOutput, AdoscError> {
454    adosc_scalar(high, low, close, volume, short, long, first, len)
455}
456
457#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
458#[inline(always)]
459pub unsafe fn adosc_avx512_short(
460    high: &[f64],
461    low: &[f64],
462    close: &[f64],
463    volume: &[f64],
464    short: usize,
465    long: usize,
466    first: usize,
467    len: usize,
468) -> Result<AdoscOutput, AdoscError> {
469    adosc_scalar(high, low, close, volume, short, long, first, len)
470}
471
472#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
473#[inline(always)]
474pub unsafe fn adosc_avx512_long(
475    high: &[f64],
476    low: &[f64],
477    close: &[f64],
478    volume: &[f64],
479    short: usize,
480    long: usize,
481    first: usize,
482    len: usize,
483) -> Result<AdoscOutput, AdoscError> {
484    adosc_scalar(high, low, close, volume, short, long, first, len)
485}
486
487#[derive(Clone, Debug)]
488pub struct AdoscBatchRange {
489    pub short_period: (usize, usize, usize),
490    pub long_period: (usize, usize, usize),
491}
492
493impl Default for AdoscBatchRange {
494    fn default() -> Self {
495        Self {
496            short_period: (3, 3, 0),
497            long_period: (10, 259, 1),
498        }
499    }
500}
501
502#[derive(Clone, Debug, Default)]
503pub struct AdoscBatchBuilder {
504    range: AdoscBatchRange,
505    kernel: Kernel,
506}
507
508impl AdoscBatchBuilder {
509    pub fn new() -> Self {
510        Self::default()
511    }
512    pub fn kernel(mut self, k: Kernel) -> Self {
513        self.kernel = k;
514        self
515    }
516    #[inline]
517    pub fn short_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
518        self.range.short_period = (start, end, step);
519        self
520    }
521    #[inline]
522    pub fn long_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
523        self.range.long_period = (start, end, step);
524        self
525    }
526    pub fn apply_slices(
527        self,
528        high: &[f64],
529        low: &[f64],
530        close: &[f64],
531        volume: &[f64],
532    ) -> Result<AdoscBatchOutput, AdoscError> {
533        adosc_batch_with_kernel(high, low, close, volume, &self.range, self.kernel)
534    }
535    pub fn apply_candles(self, candles: &Candles) -> Result<AdoscBatchOutput, AdoscError> {
536        self.apply_slices(
537            candles.high.as_slice(),
538            candles.low.as_slice(),
539            candles.close.as_slice(),
540            candles.volume.as_slice(),
541        )
542    }
543}
544
545#[derive(Clone, Debug)]
546pub struct AdoscBatchOutput {
547    pub values: Vec<f64>,
548    pub combos: Vec<AdoscParams>,
549    pub rows: usize,
550    pub cols: usize,
551}
552impl AdoscBatchOutput {
553    pub fn row_for_params(&self, p: &AdoscParams) -> Option<usize> {
554        self.combos.iter().position(|c| {
555            c.short_period.unwrap_or(3) == p.short_period.unwrap_or(3)
556                && c.long_period.unwrap_or(10) == p.long_period.unwrap_or(10)
557        })
558    }
559    pub fn values_for(&self, p: &AdoscParams) -> Option<&[f64]> {
560        self.row_for_params(p).map(|row| {
561            let start = row * self.cols;
562            &self.values[start..start + self.cols]
563        })
564    }
565}
566
567fn expand_grid(r: &AdoscBatchRange) -> Vec<AdoscParams> {
568    match expand_grid_checked(r) {
569        Ok(v) => v,
570        Err(_) => Vec::new(),
571    }
572}
573
574fn expand_grid_checked(r: &AdoscBatchRange) -> Result<Vec<AdoscParams>, AdoscError> {
575    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, AdoscError> {
576        if step == 0 || start == end {
577            return Ok(vec![start]);
578        }
579        if start < end {
580            let v: Vec<_> = (start..=end).step_by(step).collect();
581            if v.is_empty() {
582                return Err(AdoscError::InvalidRange { start, end, step });
583            }
584            Ok(v)
585        } else {
586            let mut v = Vec::new();
587            let mut cur = start;
588            while cur >= end {
589                v.push(cur);
590                if cur - end < step {
591                    break;
592                }
593                cur -= step;
594            }
595            if v.is_empty() {
596                return Err(AdoscError::InvalidRange { start, end, step });
597            }
598            Ok(v)
599        }
600    }
601    let shorts = axis_usize(r.short_period)?;
602    let longs = axis_usize(r.long_period)?;
603
604    let mut out = Vec::new();
605    for &short in &shorts {
606        for &long in &longs {
607            if short == 0 || long == 0 || short >= long {
608                continue;
609            }
610            out.push(AdoscParams {
611                short_period: Some(short),
612                long_period: Some(long),
613            });
614        }
615    }
616    if out.is_empty() {
617        return Err(AdoscError::InvalidRange {
618            start: r.short_period.0,
619            end: r.long_period.1,
620            step: r.short_period.2.max(r.long_period.2),
621        });
622    }
623    Ok(out)
624}
625
626pub fn adosc_batch_with_kernel(
627    high: &[f64],
628    low: &[f64],
629    close: &[f64],
630    volume: &[f64],
631    sweep: &AdoscBatchRange,
632    k: Kernel,
633) -> Result<AdoscBatchOutput, AdoscError> {
634    let kernel = match k {
635        Kernel::Auto => detect_best_batch_kernel(),
636        other if other.is_batch() => other,
637        other => return Err(AdoscError::InvalidKernelForBatch(other)),
638    };
639    let simd = match kernel {
640        Kernel::Avx512Batch => Kernel::Avx512,
641        Kernel::Avx2Batch => Kernel::Avx2,
642        Kernel::ScalarBatch => Kernel::Scalar,
643        _ => unreachable!(),
644    };
645    adosc_batch_par_slice(high, low, close, volume, sweep, simd)
646}
647
648pub fn adosc_batch_slice(
649    high: &[f64],
650    low: &[f64],
651    close: &[f64],
652    volume: &[f64],
653    sweep: &AdoscBatchRange,
654    kern: Kernel,
655) -> Result<AdoscBatchOutput, AdoscError> {
656    adosc_batch_inner(high, low, close, volume, sweep, kern, false)
657}
658
659pub fn adosc_batch_par_slice(
660    high: &[f64],
661    low: &[f64],
662    close: &[f64],
663    volume: &[f64],
664    sweep: &AdoscBatchRange,
665    kern: Kernel,
666) -> Result<AdoscBatchOutput, AdoscError> {
667    adosc_batch_inner(high, low, close, volume, sweep, kern, true)
668}
669
670fn adosc_batch_inner(
671    high: &[f64],
672    low: &[f64],
673    close: &[f64],
674    volume: &[f64],
675    sweep: &AdoscBatchRange,
676    kern: Kernel,
677    parallel: bool,
678) -> Result<AdoscBatchOutput, AdoscError> {
679    if high.is_empty() || low.is_empty() || close.is_empty() || volume.is_empty() {
680        return Err(AdoscError::EmptySlices {
681            high: high.len(),
682            low: low.len(),
683            close: close.len(),
684            volume: volume.len(),
685        });
686    }
687
688    let combos = expand_grid_checked(sweep)?;
689    let first = 0;
690    let len = close.len();
691    let rows = combos.len();
692    let cols = len;
693    let expected = rows
694        .checked_mul(cols)
695        .ok_or_else(|| AdoscError::InvalidInput("rows*cols overflow".into()))?;
696
697    let mut buf_mu = make_uninit_matrix(rows, cols);
698    debug_assert_eq!(buf_mu.len(), expected);
699
700    let warm: Vec<usize> = vec![0; rows];
701    init_matrix_prefixes(&mut buf_mu, cols, &warm);
702
703    let mut buf_guard = std::mem::ManuallyDrop::new(buf_mu);
704    let values: &mut [f64] = unsafe {
705        std::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
706    };
707
708    let mut adl = vec![0.0f64; len];
709    unsafe {
710        let hp = high.as_ptr();
711        let lp = low.as_ptr();
712        let cp = close.as_ptr();
713        let vp = volume.as_ptr();
714        let ap = adl.as_mut_ptr();
715
716        let h0 = *hp;
717        let l0 = *lp;
718        let c0 = *cp;
719        let v0 = *vp;
720        let hl0 = h0 - l0;
721        let mfm0 = if hl0 != 0.0 {
722            ((c0 - l0) - (h0 - c0)) / hl0
723        } else {
724            0.0
725        };
726        let mfv0 = mfm0 * v0;
727        *ap = mfv0;
728
729        let mut i = 1usize;
730        while i < len {
731            let h = *hp.add(i);
732            let l = *lp.add(i);
733            let c = *cp.add(i);
734            let v = *vp.add(i);
735            let prev = *ap.add(i - 1);
736            let hl = h - l;
737            let mfm = if hl != 0.0 {
738                ((c - l) - (h - c)) / hl
739            } else {
740                0.0
741            };
742            let mfv = mfm * v;
743            *ap.add(i) = prev + mfv;
744            i += 1;
745        }
746    }
747
748    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
749        let prm = &combos[row];
750        let short = prm.short_period.unwrap();
751        let long = prm.long_period.unwrap();
752
753        let alpha_short = 2.0 / (short as f64 + 1.0);
754        let alpha_long = 2.0 / (long as f64 + 1.0);
755        let one_minus_alpha_short = 1.0 - alpha_short;
756        let one_minus_alpha_long = 1.0 - alpha_long;
757
758        let ap = adl.as_ptr();
759        let op = out_row.as_mut_ptr();
760
761        let mut short_ema = *ap;
762        let mut long_ema = *ap;
763        *op = short_ema - long_ema;
764
765        let mut i = 1usize;
766        while i < cols {
767            let s = *ap.add(i);
768            short_ema = alpha_short * s + one_minus_alpha_short * short_ema;
769            long_ema = alpha_long * s + one_minus_alpha_long * long_ema;
770            *op.add(i) = short_ema - long_ema;
771            i += 1;
772        }
773    };
774    if parallel {
775        #[cfg(not(target_arch = "wasm32"))]
776        {
777            values
778                .par_chunks_mut(cols)
779                .enumerate()
780                .for_each(|(row, slice)| do_row(row, slice));
781        }
782
783        #[cfg(target_arch = "wasm32")]
784        {
785            for (row, slice) in values.chunks_mut(cols).enumerate() {
786                do_row(row, slice);
787            }
788        }
789    } else {
790        for (row, slice) in values.chunks_mut(cols).enumerate() {
791            do_row(row, slice);
792        }
793    }
794
795    let values = unsafe {
796        Vec::from_raw_parts(
797            buf_guard.as_mut_ptr() as *mut f64,
798            buf_guard.len(),
799            buf_guard.capacity(),
800        )
801    };
802
803    Ok(AdoscBatchOutput {
804        values,
805        combos,
806        rows,
807        cols,
808    })
809}
810
811#[inline(always)]
812pub fn adosc_batch_inner_into(
813    high: &[f64],
814    low: &[f64],
815    close: &[f64],
816    volume: &[f64],
817    sweep: &AdoscBatchRange,
818    kern: Kernel,
819    parallel: bool,
820    out: &mut [f64],
821) -> Result<Vec<AdoscParams>, AdoscError> {
822    if high.is_empty() || low.is_empty() || close.is_empty() || volume.is_empty() {
823        return Err(AdoscError::EmptySlices {
824            high: high.len(),
825            low: low.len(),
826            close: close.len(),
827            volume: volume.len(),
828        });
829    }
830
831    let combos = expand_grid_checked(sweep)?;
832    let first = 0;
833    let len = close.len();
834    let rows = combos.len();
835    let cols = len;
836    let expected = rows
837        .checked_mul(cols)
838        .ok_or_else(|| AdoscError::InvalidInput("rows*cols overflow".into()))?;
839    if out.len() != expected {
840        return Err(AdoscError::OutputLengthMismatch {
841            expected,
842            got: out.len(),
843        });
844    }
845
846    let mut adl = vec![0.0f64; len];
847    unsafe {
848        let hp = high.as_ptr();
849        let lp = low.as_ptr();
850        let cp = close.as_ptr();
851        let vp = volume.as_ptr();
852        let ap = adl.as_mut_ptr();
853
854        let h0 = *hp;
855        let l0 = *lp;
856        let c0 = *cp;
857        let v0 = *vp;
858        let hl0 = h0 - l0;
859        let mfm0 = if hl0 != 0.0 {
860            ((c0 - l0) - (h0 - c0)) / hl0
861        } else {
862            0.0
863        };
864        let mfv0 = mfm0 * v0;
865        *ap = mfv0;
866
867        let mut i = 1usize;
868        while i < len {
869            let h = *hp.add(i);
870            let l = *lp.add(i);
871            let c = *cp.add(i);
872            let v = *vp.add(i);
873            let prev = *ap.add(i - 1);
874            let hl = h - l;
875            let mfm = if hl != 0.0 {
876                ((c - l) - (h - c)) / hl
877            } else {
878                0.0
879            };
880            let mfv = mfm * v;
881            *ap.add(i) = prev + mfv;
882            i += 1;
883        }
884    }
885
886    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
887        let prm = &combos[row];
888        let short = prm.short_period.unwrap();
889        let long = prm.long_period.unwrap();
890
891        let alpha_short = 2.0 / (short as f64 + 1.0);
892        let alpha_long = 2.0 / (long as f64 + 1.0);
893        let one_minus_alpha_short = 1.0 - alpha_short;
894        let one_minus_alpha_long = 1.0 - alpha_long;
895
896        let ap = adl.as_ptr();
897        let op = out_row.as_mut_ptr();
898        let mut short_ema = *ap;
899        let mut long_ema = *ap;
900        *op = short_ema - long_ema;
901
902        let mut i = 1usize;
903        while i < cols {
904            let s = *ap.add(i);
905            short_ema = alpha_short * s + one_minus_alpha_short * short_ema;
906            long_ema = alpha_long * s + one_minus_alpha_long * long_ema;
907            *op.add(i) = short_ema - long_ema;
908            i += 1;
909        }
910    };
911
912    if parallel {
913        #[cfg(not(target_arch = "wasm32"))]
914        {
915            out.par_chunks_mut(cols)
916                .enumerate()
917                .for_each(|(row, slice)| do_row(row, slice));
918        }
919
920        #[cfg(target_arch = "wasm32")]
921        {
922            for (row, slice) in out.chunks_mut(cols).enumerate() {
923                do_row(row, slice);
924            }
925        }
926    } else {
927        for (row, slice) in out.chunks_mut(cols).enumerate() {
928            do_row(row, slice);
929        }
930    }
931
932    Ok(combos)
933}
934
935#[inline(always)]
936pub unsafe fn adosc_row_scalar(
937    high: &[f64],
938    low: &[f64],
939    close: &[f64],
940    volume: &[f64],
941    short: usize,
942    long: usize,
943    _first: usize,
944    out: &mut [f64],
945) -> Result<(), AdoscError> {
946    let len = out.len();
947    debug_assert!(len > 0);
948
949    let alpha_short = 2.0 / (short as f64 + 1.0);
950    let alpha_long = 2.0 / (long as f64 + 1.0);
951    let one_minus_alpha_short = 1.0 - alpha_short;
952    let one_minus_alpha_long = 1.0 - alpha_long;
953
954    let hp = high.as_ptr();
955    let lp = low.as_ptr();
956    let cp = close.as_ptr();
957    let vp = volume.as_ptr();
958    let op = out.as_mut_ptr();
959
960    let h0 = *hp;
961    let l0 = *lp;
962    let c0 = *cp;
963    let v0 = *vp;
964    let hl0 = h0 - l0;
965    let mfm0 = if hl0 != 0.0 {
966        ((c0 - l0) - (h0 - c0)) / hl0
967    } else {
968        0.0
969    };
970    let mfv0 = mfm0 * v0;
971    let mut sum_ad = mfv0;
972    let mut short_ema = sum_ad;
973    let mut long_ema = sum_ad;
974    *op = short_ema - long_ema;
975
976    let mut i = 1usize;
977    while i < len {
978        let h = *hp.add(i);
979        let l = *lp.add(i);
980        let c = *cp.add(i);
981        let v = *vp.add(i);
982
983        let hl = h - l;
984        let mfm = if hl != 0.0 {
985            ((c - l) - (h - c)) / hl
986        } else {
987            0.0
988        };
989        let mfv = mfm * v;
990        sum_ad += mfv;
991        short_ema = alpha_short * sum_ad + one_minus_alpha_short * short_ema;
992        long_ema = alpha_long * sum_ad + one_minus_alpha_long * long_ema;
993        *op.add(i) = short_ema - long_ema;
994
995        i += 1;
996    }
997    Ok(())
998}
999
1000#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1001#[inline(always)]
1002pub unsafe fn adosc_row_avx2(
1003    high: &[f64],
1004    low: &[f64],
1005    close: &[f64],
1006    volume: &[f64],
1007    short: usize,
1008    long: usize,
1009    first: usize,
1010    out: &mut [f64],
1011) -> Result<(), AdoscError> {
1012    adosc_row_scalar(high, low, close, volume, short, long, first, out)
1013}
1014
1015#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1016#[inline(always)]
1017pub unsafe fn adosc_row_avx512(
1018    high: &[f64],
1019    low: &[f64],
1020    close: &[f64],
1021    volume: &[f64],
1022    short: usize,
1023    long: usize,
1024    first: usize,
1025    out: &mut [f64],
1026) -> Result<(), AdoscError> {
1027    adosc_row_scalar(high, low, close, volume, short, long, first, out)
1028}
1029
1030#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1031#[inline(always)]
1032pub unsafe fn adosc_row_avx512_short(
1033    high: &[f64],
1034    low: &[f64],
1035    close: &[f64],
1036    volume: &[f64],
1037    short: usize,
1038    long: usize,
1039    first: usize,
1040    out: &mut [f64],
1041) -> Result<(), AdoscError> {
1042    adosc_row_scalar(high, low, close, volume, short, long, first, out)
1043}
1044
1045#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1046#[inline(always)]
1047pub unsafe fn adosc_row_avx512_long(
1048    high: &[f64],
1049    low: &[f64],
1050    close: &[f64],
1051    volume: &[f64],
1052    short: usize,
1053    long: usize,
1054    first: usize,
1055    out: &mut [f64],
1056) -> Result<(), AdoscError> {
1057    adosc_row_scalar(high, low, close, volume, short, long, first, out)
1058}
1059
1060pub struct AdoscStream {
1061    short_period: usize,
1062    long_period: usize,
1063
1064    alpha_short: f64,
1065    alpha_long: f64,
1066    one_minus_alpha_short: f64,
1067    one_minus_alpha_long: f64,
1068
1069    sum_ad: f64,
1070    short_ema: f64,
1071    long_ema: f64,
1072    initialized: bool,
1073}
1074
1075impl AdoscStream {
1076    #[inline(always)]
1077    pub fn try_new(params: AdoscParams) -> Result<Self, AdoscError> {
1078        let short = params.short_period.unwrap_or(3);
1079        let long = params.long_period.unwrap_or(10);
1080        if short == 0 || long == 0 {
1081            return Err(AdoscError::InvalidPeriod {
1082                short,
1083                long,
1084                data_len: 0,
1085            });
1086        }
1087        if short >= long {
1088            return Err(AdoscError::ShortPeriodGreaterThanLong { short, long });
1089        }
1090
1091        let alpha_short = 2.0 / (short as f64 + 1.0);
1092        let alpha_long = 2.0 / (long as f64 + 1.0);
1093
1094        Ok(Self {
1095            short_period: short,
1096            long_period: long,
1097            alpha_short,
1098            alpha_long,
1099            one_minus_alpha_short: 1.0 - alpha_short,
1100            one_minus_alpha_long: 1.0 - alpha_long,
1101            sum_ad: 0.0,
1102            short_ema: 0.0,
1103            long_ema: 0.0,
1104            initialized: false,
1105        })
1106    }
1107
1108    #[inline(always)]
1109    pub fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> f64 {
1110        if volume != 0.0 {
1111            let hl = high - low;
1112            if hl != 0.0 {
1113                let mfm = ((close - low) - (high - close)) / hl;
1114                self.sum_ad += mfm * volume;
1115            }
1116        }
1117
1118        if !self.initialized {
1119            self.short_ema = self.sum_ad;
1120            self.long_ema = self.sum_ad;
1121            self.initialized = true;
1122            return 0.0;
1123        }
1124
1125        let x = self.sum_ad;
1126        self.short_ema = self.alpha_short * x + self.one_minus_alpha_short * self.short_ema;
1127        self.long_ema = self.alpha_long * x + self.one_minus_alpha_long * self.long_ema;
1128
1129        self.short_ema - self.long_ema
1130    }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135    use super::*;
1136    use crate::skip_if_unsupported;
1137    use crate::utilities::data_loader::read_candles_from_csv;
1138
1139    fn check_adosc_accuracy(
1140        test_name: &str,
1141        kernel: Kernel,
1142    ) -> Result<(), Box<dyn std::error::Error>> {
1143        skip_if_unsupported!(kernel, test_name);
1144        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1145        let candles = read_candles_from_csv(file_path)?;
1146        let input = AdoscInput::with_default_candles(&candles);
1147        let result = adosc_with_kernel(&input, kernel)?;
1148        assert_eq!(result.values.len(), candles.close.len());
1149        let expected_last_five = [-166.2175, -148.9983, -144.9052, -128.5921, -142.0772];
1150        let start_index = result.values.len().saturating_sub(5);
1151        let result_last_five = &result.values[start_index..];
1152        for (i, &actual) in result_last_five.iter().enumerate() {
1153            let expected = expected_last_five[i];
1154            assert!(
1155                (actual - expected).abs() < 1e-1,
1156                "ADOSC value mismatch at index {}: expected {}, got {}",
1157                i,
1158                expected,
1159                actual
1160            );
1161        }
1162        for (i, &val) in result.values.iter().enumerate() {
1163            assert!(
1164                val.is_finite(),
1165                "ADOSC output at index {} should be finite, got {}",
1166                i,
1167                val
1168            );
1169        }
1170        Ok(())
1171    }
1172
1173    fn check_adosc_partial_params(
1174        test_name: &str,
1175        kernel: Kernel,
1176    ) -> Result<(), Box<dyn std::error::Error>> {
1177        skip_if_unsupported!(kernel, test_name);
1178        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1179        let candles = read_candles_from_csv(file_path)?;
1180        let partial_params = AdoscParams {
1181            short_period: Some(2),
1182            long_period: None,
1183        };
1184        let input = AdoscInput::from_candles(&candles, partial_params);
1185        let result = adosc_with_kernel(&input, kernel)?;
1186        assert_eq!(result.values.len(), candles.close.len());
1187        let missing_short = AdoscParams {
1188            short_period: None,
1189            long_period: Some(12),
1190        };
1191        let input_missing = AdoscInput::from_candles(&candles, missing_short);
1192        let result_missing = adosc_with_kernel(&input_missing, kernel)?;
1193        assert_eq!(result_missing.values.len(), candles.close.len());
1194        Ok(())
1195    }
1196
1197    fn check_adosc_default_candles(
1198        test_name: &str,
1199        kernel: Kernel,
1200    ) -> Result<(), Box<dyn std::error::Error>> {
1201        skip_if_unsupported!(kernel, test_name);
1202        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1203        let candles = read_candles_from_csv(file_path)?;
1204        let input = AdoscInput::with_default_candles(&candles);
1205        match input.data {
1206            AdoscData::Candles { .. } => {}
1207            _ => panic!("Expected AdoscData::Candles variant"),
1208        }
1209        let result = adosc_with_kernel(&input, kernel)?;
1210        assert_eq!(result.values.len(), candles.close.len());
1211        Ok(())
1212    }
1213
1214    fn check_adosc_zero_period(
1215        test_name: &str,
1216        kernel: Kernel,
1217    ) -> Result<(), Box<dyn std::error::Error>> {
1218        skip_if_unsupported!(kernel, test_name);
1219        let high = [10.0, 10.0, 10.0];
1220        let low = [5.0, 5.0, 5.0];
1221        let close = [7.0, 7.0, 7.0];
1222        let volume = [1000.0, 1000.0, 1000.0];
1223        let zero_short = AdoscParams {
1224            short_period: Some(0),
1225            long_period: Some(10),
1226        };
1227        let input = AdoscInput::from_slices(&high, &low, &close, &volume, zero_short);
1228        let result = adosc_with_kernel(&input, kernel);
1229        assert!(result.is_err());
1230        let zero_long = AdoscParams {
1231            short_period: Some(3),
1232            long_period: Some(0),
1233        };
1234        let input2 = AdoscInput::from_slices(&high, &low, &close, &volume, zero_long);
1235        let result2 = adosc_with_kernel(&input2, kernel);
1236        assert!(result2.is_err());
1237        Ok(())
1238    }
1239
1240    fn check_adosc_period_exceeds_length(
1241        test_name: &str,
1242        kernel: Kernel,
1243    ) -> Result<(), Box<dyn std::error::Error>> {
1244        skip_if_unsupported!(kernel, test_name);
1245        let high = [10.0, 11.0, 12.0];
1246        let low = [5.0, 5.5, 6.0];
1247        let close = [7.0, 8.0, 9.0];
1248        let volume = [1000.0, 1000.0, 1000.0];
1249        let params = AdoscParams {
1250            short_period: Some(3),
1251            long_period: Some(10),
1252        };
1253        let input = AdoscInput::from_slices(&high, &low, &close, &volume, params);
1254        let result = adosc_with_kernel(&input, kernel);
1255        assert!(result.is_err());
1256        Ok(())
1257    }
1258
1259    fn check_adosc_very_small_dataset(
1260        test_name: &str,
1261        kernel: Kernel,
1262    ) -> Result<(), Box<dyn std::error::Error>> {
1263        skip_if_unsupported!(kernel, test_name);
1264        let high = [10.0];
1265        let low = [5.0];
1266        let close = [7.0];
1267        let volume = [1000.0];
1268        let params = AdoscParams {
1269            short_period: Some(3),
1270            long_period: Some(10),
1271        };
1272        let input = AdoscInput::from_slices(&high, &low, &close, &volume, params);
1273        let result = adosc_with_kernel(&input, kernel);
1274        assert!(result.is_err());
1275        Ok(())
1276    }
1277
1278    fn check_adosc_reinput(
1279        test_name: &str,
1280        kernel: Kernel,
1281    ) -> Result<(), Box<dyn std::error::Error>> {
1282        skip_if_unsupported!(kernel, test_name);
1283        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1284        let candles = read_candles_from_csv(file_path)?;
1285        let first_params = AdoscParams {
1286            short_period: Some(3),
1287            long_period: Some(10),
1288        };
1289        let first_input = AdoscInput::from_candles(&candles, first_params);
1290        let first_result = adosc_with_kernel(&first_input, kernel)?;
1291        assert_eq!(first_result.values.len(), candles.close.len());
1292        let second_params = AdoscParams {
1293            short_period: Some(2),
1294            long_period: Some(6),
1295        };
1296        let second_input = AdoscInput::from_slices(
1297            &first_result.values,
1298            &first_result.values,
1299            &first_result.values,
1300            &first_result.values,
1301            second_params,
1302        );
1303        let second_result = adosc_with_kernel(&second_input, kernel)?;
1304        assert_eq!(second_result.values.len(), first_result.values.len());
1305        Ok(())
1306    }
1307
1308    fn check_adosc_nan_handling(
1309        test_name: &str,
1310        kernel: Kernel,
1311    ) -> Result<(), Box<dyn std::error::Error>> {
1312        skip_if_unsupported!(kernel, test_name);
1313        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1314        let candles = read_candles_from_csv(file_path)?;
1315        let input = AdoscInput::from_candles(&candles, AdoscParams::default());
1316        let result = adosc_with_kernel(&input, kernel)?;
1317        assert_eq!(result.values.len(), candles.close.len());
1318        if result.values.len() > 240 {
1319            for (i, &val) in result.values[240..].iter().enumerate() {
1320                assert!(
1321                    !val.is_nan(),
1322                    "[{}] Found unexpected NaN at out-index {}",
1323                    test_name,
1324                    240 + i
1325                );
1326            }
1327        }
1328        Ok(())
1329    }
1330
1331    fn check_adosc_streaming(
1332        test_name: &str,
1333        kernel: Kernel,
1334    ) -> Result<(), Box<dyn std::error::Error>> {
1335        skip_if_unsupported!(kernel, test_name);
1336        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1337        let candles = read_candles_from_csv(file_path)?;
1338        let params = AdoscParams {
1339            short_period: Some(3),
1340            long_period: Some(10),
1341        };
1342        let input = AdoscInput::from_candles(&candles, params.clone());
1343        let batch_output = adosc_with_kernel(&input, kernel)?.values;
1344        let mut stream = AdoscStream::try_new(params)?;
1345        let mut stream_values = Vec::with_capacity(candles.close.len());
1346        for ((&h, &l), (&c, &v)) in candles
1347            .high
1348            .iter()
1349            .zip(candles.low.iter())
1350            .zip(candles.close.iter().zip(candles.volume.iter()))
1351        {
1352            stream_values.push(stream.update(h, l, c, v));
1353        }
1354        assert_eq!(batch_output.len(), stream_values.len());
1355        for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
1356            let diff = (b - s).abs();
1357            assert!(
1358                diff < 1e-9,
1359                "[{}] ADOSC streaming mismatch at idx {}: batch={}, stream={}, diff={}",
1360                test_name,
1361                i,
1362                b,
1363                s,
1364                diff
1365            );
1366        }
1367        Ok(())
1368    }
1369
1370    fn check_batch_default_row(
1371        test: &str,
1372        kernel: Kernel,
1373    ) -> Result<(), Box<dyn std::error::Error>> {
1374        skip_if_unsupported!(kernel, test);
1375        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1376        let candles = read_candles_from_csv(file_path)?;
1377        let batch = AdoscBatchBuilder::new()
1378            .kernel(kernel)
1379            .apply_candles(&candles)?;
1380        let def = AdoscParams::default();
1381        let row = batch.values_for(&def).expect("default row missing");
1382        assert_eq!(row.len(), candles.close.len());
1383        Ok(())
1384    }
1385
1386    macro_rules! generate_all_adosc_tests {
1387        ($($test_fn:ident),*) => {
1388            paste::paste! {
1389                $(
1390                    #[test]
1391                    fn [<$test_fn _scalar_f64>]() { let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar); }
1392                )*
1393                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1394                $(
1395                    #[test]
1396                    fn [<$test_fn _avx2_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2); }
1397                    #[test]
1398                    fn [<$test_fn _avx512_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512); }
1399                )*
1400            }
1401        }
1402    }
1403
1404    fn check_adosc_no_poison(
1405        test_name: &str,
1406        kernel: Kernel,
1407    ) -> Result<(), Box<dyn std::error::Error>> {
1408        skip_if_unsupported!(kernel, test_name);
1409        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1410        let candles = read_candles_from_csv(file_path)?;
1411
1412        let len = candles.close.len();
1413        let mut high = AVec::<f64>::with_capacity(CACHELINE_ALIGN, len);
1414        let mut low = AVec::<f64>::with_capacity(CACHELINE_ALIGN, len);
1415        let mut close = AVec::<f64>::with_capacity(CACHELINE_ALIGN, len);
1416        let mut volume = AVec::<f64>::with_capacity(CACHELINE_ALIGN, len);
1417
1418        high.resize(len, f64::from_bits(0x11111111_11111111));
1419        low.resize(len, f64::from_bits(0x22222222_22222222));
1420        close.resize(len, f64::from_bits(0x33333333_33333333));
1421        volume.resize(len, f64::from_bits(0x11111111_11111111));
1422
1423        high.copy_from_slice(&candles.high);
1424        low.copy_from_slice(&candles.low);
1425        close.copy_from_slice(&candles.close);
1426        volume.copy_from_slice(&candles.volume);
1427
1428        let params = AdoscParams {
1429            short_period: Some(3),
1430            long_period: Some(10),
1431        };
1432        let input = AdoscInput::from_slices(&high, &low, &close, &volume, params);
1433        let result = adosc_with_kernel(&input, kernel)?;
1434
1435        for (i, &val) in result.values.iter().enumerate() {
1436            assert_ne!(
1437                val.to_bits(),
1438                0x11111111_11111111,
1439                "[{}] Poison value 0x11111111_11111111 found at index {}",
1440                test_name,
1441                i
1442            );
1443            assert_ne!(
1444                val.to_bits(),
1445                0x22222222_22222222,
1446                "[{}] Poison value 0x22222222_22222222 found at index {}",
1447                test_name,
1448                i
1449            );
1450            assert_ne!(
1451                val.to_bits(),
1452                0x33333333_33333333,
1453                "[{}] Poison value 0x33333333_33333333 found at index {}",
1454                test_name,
1455                i
1456            );
1457        }
1458
1459        Ok(())
1460    }
1461
1462    fn check_batch_no_poison(
1463        test_name: &str,
1464        kernel: Kernel,
1465    ) -> Result<(), Box<dyn std::error::Error>> {
1466        skip_if_unsupported!(kernel, test_name);
1467        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1468        let candles = read_candles_from_csv(file_path)?;
1469
1470        let slice_end = candles.close.len().min(1000);
1471        let high_slice = &candles.high[..slice_end];
1472        let low_slice = &candles.low[..slice_end];
1473        let close_slice = &candles.close[..slice_end];
1474        let volume_slice = &candles.volume[..slice_end];
1475
1476        let batch_config = AdoscBatchRange {
1477            short_period: (2, 5, 1),
1478            long_period: (8, 12, 2),
1479        };
1480
1481        let result = adosc_batch_with_kernel(
1482            high_slice,
1483            low_slice,
1484            close_slice,
1485            volume_slice,
1486            &batch_config,
1487            kernel,
1488        )?;
1489
1490        for (i, &val) in result.values.iter().enumerate() {
1491            assert_ne!(
1492                val.to_bits(),
1493                0x11111111_11111111,
1494                "[{}] Poison value 0x11111111_11111111 found in batch output at index {}",
1495                test_name,
1496                i
1497            );
1498            assert_ne!(
1499                val.to_bits(),
1500                0x22222222_22222222,
1501                "[{}] Poison value 0x22222222_22222222 found in batch output at index {}",
1502                test_name,
1503                i
1504            );
1505            assert_ne!(
1506                val.to_bits(),
1507                0x33333333_33333333,
1508                "[{}] Poison value 0x33333333_33333333 found in batch output at index {}",
1509                test_name,
1510                i
1511            );
1512        }
1513
1514        let expected_rows = result.combos.len();
1515        let expected_cols = slice_end;
1516        assert_eq!(
1517            result.values.len(),
1518            expected_rows * expected_cols,
1519            "[{}] Batch output size mismatch",
1520            test_name
1521        );
1522
1523        let batch_config2 = AdoscBatchRange {
1524            short_period: (3, 7, 2),
1525            long_period: (10, 20, 5),
1526        };
1527
1528        let result2 = adosc_batch_with_kernel(
1529            high_slice,
1530            low_slice,
1531            close_slice,
1532            volume_slice,
1533            &batch_config2,
1534            kernel,
1535        )?;
1536
1537        for (i, &val) in result2.values.iter().enumerate() {
1538            assert_ne!(
1539                val.to_bits(),
1540                0x11111111_11111111,
1541                "[{}] Poison value found in second batch config at index {}",
1542                test_name,
1543                i
1544            );
1545        }
1546
1547        Ok(())
1548    }
1549
1550    #[cfg(feature = "proptest")]
1551    #[allow(clippy::float_cmp)]
1552    fn check_adosc_property(
1553        test_name: &str,
1554        kernel: Kernel,
1555    ) -> Result<(), Box<dyn std::error::Error>> {
1556        use proptest::prelude::*;
1557        skip_if_unsupported!(kernel, test_name);
1558
1559        let strat = (1usize..=10, 11usize..=30).prop_flat_map(|(short_period, long_period)| {
1560            let len = long_period..400;
1561            (
1562                prop::collection::vec(
1563                    (1f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
1564                    len.clone(),
1565                )
1566                .prop_flat_map(move |base_prices| {
1567                    let len = base_prices.len();
1568
1569                    let high_spreads = prop::collection::vec(
1570                        (0f64..100f64).prop_filter("finite", |x| x.is_finite()),
1571                        len,
1572                    );
1573                    let low_spreads = prop::collection::vec(
1574                        (0f64..100f64).prop_filter("finite", |x| x.is_finite()),
1575                        len,
1576                    );
1577
1578                    let close_positions = prop::collection::vec(0f64..=1f64, len);
1579
1580                    (
1581                        Just(base_prices),
1582                        high_spreads,
1583                        low_spreads,
1584                        close_positions,
1585                    )
1586                })
1587                .prop_map(|(base, high_spreads, low_spreads, close_positions)| {
1588                    let mut high = Vec::with_capacity(base.len());
1589                    let mut low = Vec::with_capacity(base.len());
1590                    let mut close = Vec::with_capacity(base.len());
1591
1592                    for i in 0..base.len() {
1593                        let h = base[i] + high_spreads[i];
1594                        let l = base[i] - low_spreads[i];
1595                        let c = l + (h - l) * close_positions[i];
1596
1597                        high.push(h);
1598                        low.push(l);
1599                        close.push(c);
1600                    }
1601
1602                    (high, low, close)
1603                }),
1604                prop::collection::vec((0f64..1e6f64).prop_filter("finite", |x| x.is_finite()), len),
1605                Just(short_period),
1606                Just(long_period),
1607            )
1608        });
1609
1610        proptest::test_runner::TestRunner::default()
1611            .run(
1612                &strat,
1613                |((high, low, close), volume, short_period, long_period)| {
1614                    let len = high.len();
1615                    prop_assert_eq!(low.len(), len);
1616                    prop_assert_eq!(close.len(), len);
1617                    prop_assert_eq!(volume.len(), len);
1618
1619                    for i in 0..len {
1620                        prop_assert!(
1621                            high[i] >= low[i],
1622                            "High must be >= Low at index {}: {} < {}",
1623                            i,
1624                            high[i],
1625                            low[i]
1626                        );
1627                        prop_assert!(
1628                            close[i] >= low[i] && close[i] <= high[i],
1629                            "Close must be between Low and High at index {}: {} not in [{}, {}]",
1630                            i,
1631                            close[i],
1632                            low[i],
1633                            high[i]
1634                        );
1635                    }
1636
1637                    let params = AdoscParams {
1638                        short_period: Some(short_period),
1639                        long_period: Some(long_period),
1640                    };
1641                    let input = AdoscInput::from_slices(&high, &low, &close, &volume, params);
1642
1643                    let result = adosc_with_kernel(&input, kernel);
1644                    prop_assert!(result.is_ok(), "ADOSC computation failed: {:?}", result);
1645
1646                    let AdoscOutput { values: out } = result.unwrap();
1647
1648                    prop_assert_eq!(out.len(), len, "Output length mismatch");
1649
1650                    for (i, &val) in out.iter().enumerate() {
1651                        prop_assert!(
1652                            val.is_finite(),
1653                            "ADOSC output at index {} should be finite, got {}",
1654                            i,
1655                            val
1656                        );
1657                    }
1658
1659                    prop_assert!(
1660                        out[0].abs() < 1e-10,
1661                        "First ADOSC value should be 0, got {}",
1662                        out[0]
1663                    );
1664
1665                    if volume.iter().all(|&v| v == 0.0) {
1666                        for &val in out.iter() {
1667                            prop_assert!(
1668                                val.abs() < 1e-9,
1669                                "With zero volume, ADOSC should be ~0, got {}",
1670                                val
1671                            );
1672                        }
1673                    }
1674
1675                    for i in 0..len {
1676                        let h = high[i];
1677                        let l = low[i];
1678                        let c = close[i];
1679                        let hl = h - l;
1680                        if hl != 0.0 {
1681                            let mfm = ((c - l) - (h - c)) / hl;
1682                            prop_assert!(
1683                                mfm >= -1.0 - 1e-10 && mfm <= 1.0 + 1e-10,
1684                                "MFM at index {} out of bounds: {}",
1685                                i,
1686                                mfm
1687                            );
1688                        }
1689                    }
1690
1691                    let total_volume: f64 = volume.iter().sum();
1692
1693                    let expected_bound = total_volume * 0.5;
1694                    for (i, &val) in out.iter().enumerate() {
1695                        prop_assert!(
1696                            val.abs() <= expected_bound,
1697                            "ADOSC at index {} exceeds reasonable bounds: {} > {}",
1698                            i,
1699                            val.abs(),
1700                            expected_bound
1701                        );
1702                    }
1703
1704                    prop_assert!(
1705                        short_period < long_period,
1706                        "Short period must be less than long period"
1707                    );
1708
1709                    if len >= 3 {
1710                        let alpha_short = 2.0 / (short_period as f64 + 1.0);
1711                        let alpha_long = 2.0 / (long_period as f64 + 1.0);
1712
1713                        let h0 = high[0];
1714                        let l0 = low[0];
1715                        let c0 = close[0];
1716                        let v0 = volume[0];
1717                        let hl0 = h0 - l0;
1718                        let mfm0 = if hl0 != 0.0 {
1719                            ((c0 - l0) - (h0 - c0)) / hl0
1720                        } else {
1721                            0.0
1722                        };
1723                        let mfv0 = mfm0 * v0;
1724                        let sum_ad0 = mfv0;
1725                        let expected_first = 0.0;
1726                        prop_assert!(
1727                            (out[0] - expected_first).abs() < 1e-9,
1728                            "First value mismatch: expected {}, got {}",
1729                            expected_first,
1730                            out[0]
1731                        );
1732
1733                        let h1 = high[1];
1734                        let l1 = low[1];
1735                        let c1 = close[1];
1736                        let v1 = volume[1];
1737                        let hl1 = h1 - l1;
1738                        let mfm1 = if hl1 != 0.0 {
1739                            ((c1 - l1) - (h1 - c1)) / hl1
1740                        } else {
1741                            0.0
1742                        };
1743                        let mfv1 = mfm1 * v1;
1744                        let sum_ad1 = sum_ad0 + mfv1;
1745                        let short_ema1 = alpha_short * sum_ad1 + (1.0 - alpha_short) * sum_ad0;
1746                        let long_ema1 = alpha_long * sum_ad1 + (1.0 - alpha_long) * sum_ad0;
1747                        let expected_second = short_ema1 - long_ema1;
1748                        prop_assert!(
1749                            (out[1] - expected_second).abs() < 1e-9,
1750                            "Second value mismatch: expected {}, got {}",
1751                            expected_second,
1752                            out[1]
1753                        );
1754                    }
1755
1756                    let ref_output = adosc_with_kernel(&input, Kernel::Scalar);
1757                    prop_assert!(ref_output.is_ok(), "Reference scalar computation failed");
1758                    let AdoscOutput { values: ref_out } = ref_output.unwrap();
1759
1760                    for (i, (&val, &ref_val)) in out.iter().zip(ref_out.iter()).enumerate() {
1761                        let val_bits = val.to_bits();
1762                        let ref_bits = ref_val.to_bits();
1763
1764                        if !val.is_finite() || !ref_val.is_finite() {
1765                            prop_assert_eq!(
1766                                val_bits,
1767                                ref_bits,
1768                                "NaN/Inf mismatch at index {}: {} vs {}",
1769                                i,
1770                                val,
1771                                ref_val
1772                            );
1773                        } else {
1774                            let ulp_diff = val_bits.abs_diff(ref_bits);
1775                            prop_assert!(
1776                                (val - ref_val).abs() <= 1e-9 || ulp_diff <= 4,
1777                                "Kernel mismatch at index {}: {} vs {} (diff: {}, ULP: {})",
1778                                i,
1779                                val,
1780                                ref_val,
1781                                (val - ref_val).abs(),
1782                                ulp_diff
1783                            );
1784                        }
1785                    }
1786
1787                    Ok(())
1788                },
1789            )
1790            .map_err(|e| e.into())
1791    }
1792
1793    #[cfg(not(feature = "proptest"))]
1794    fn check_adosc_property(
1795        test_name: &str,
1796        kernel: Kernel,
1797    ) -> Result<(), Box<dyn std::error::Error>> {
1798        skip_if_unsupported!(kernel, test_name);
1799        Ok(())
1800    }
1801
1802    generate_all_adosc_tests!(
1803        check_adosc_accuracy,
1804        check_adosc_partial_params,
1805        check_adosc_default_candles,
1806        check_adosc_zero_period,
1807        check_adosc_period_exceeds_length,
1808        check_adosc_very_small_dataset,
1809        check_adosc_reinput,
1810        check_adosc_nan_handling,
1811        check_adosc_streaming,
1812        check_adosc_no_poison,
1813        check_adosc_property
1814    );
1815
1816    macro_rules! gen_batch_tests {
1817        ($fn_name:ident) => {
1818            paste::paste! {
1819                #[test] fn [<$fn_name _scalar>]()      { let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch); }
1820                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1821                #[test] fn [<$fn_name _avx2>]()        { let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch); }
1822                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1823                #[test] fn [<$fn_name _avx512>]()      { let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch); }
1824                #[test] fn [<$fn_name _auto_detect>]() { let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto); }
1825            }
1826        };
1827    }
1828
1829    gen_batch_tests!(check_batch_default_row);
1830    gen_batch_tests!(check_batch_no_poison);
1831
1832    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1833    #[test]
1834    fn test_adosc_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
1835        let len = 512usize;
1836        let mut high = Vec::with_capacity(len);
1837        let mut low = Vec::with_capacity(len);
1838        let mut close = Vec::with_capacity(len);
1839        let mut volume = Vec::with_capacity(len);
1840
1841        for i in 0..len {
1842            let base = 100.0 + (i as f64) * 0.05 + ((i % 13) as f64) * 0.01;
1843            let spread = 0.5 + ((i % 7) as f64) * 0.03;
1844            let lo = base - spread;
1845            let hi = base + spread;
1846
1847            let frac = ((i % 97) as f64) / 96.0;
1848            let cl = lo + (hi - lo) * frac;
1849            let vol = 1_000.0 + ((i * 37) % 10_000) as f64;
1850
1851            low.push(lo);
1852            high.push(hi);
1853            close.push(cl);
1854            volume.push(vol);
1855        }
1856
1857        let input = AdoscInput::from_slices(&high, &low, &close, &volume, AdoscParams::default());
1858
1859        let baseline = adosc(&input)?.values;
1860
1861        let mut out = vec![0.0; len];
1862        adosc_into(&input, &mut out)?;
1863
1864        assert_eq!(baseline.len(), out.len());
1865
1866        #[inline]
1867        fn eq_or_both_nan(a: f64, b: f64) -> bool {
1868            (a.is_nan() && b.is_nan()) || (a == b) || (a - b).abs() <= 1e-12
1869        }
1870
1871        for (i, (&a, &b)) in baseline.iter().zip(out.iter()).enumerate() {
1872            assert!(
1873                eq_or_both_nan(a, b),
1874                "ADOSC parity mismatch at index {}: api={}, into={}",
1875                i,
1876                a,
1877                b
1878            );
1879        }
1880        Ok(())
1881    }
1882}
1883
1884#[inline]
1885pub fn adosc_into_slice(
1886    dst: &mut [f64],
1887    input: &AdoscInput,
1888    kern: Kernel,
1889) -> Result<(), AdoscError> {
1890    let (high, low, close, volume, short, long, first, len, chosen) = adosc_prepare(input, kern)?;
1891    if dst.len() != len {
1892        return Err(AdoscError::OutputLengthMismatch {
1893            expected: len,
1894            got: dst.len(),
1895        });
1896    }
1897    unsafe {
1898        match chosen {
1899            Kernel::Scalar | Kernel::ScalarBatch => {
1900                adosc_row_scalar(high, low, close, volume, short, long, first, dst)?
1901            }
1902            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1903            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
1904                adosc_row_scalar(high, low, close, volume, short, long, first, dst)?
1905            }
1906            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
1907            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
1908                adosc_row_scalar(high, low, close, volume, short, long, first, dst)?
1909            }
1910            _ => unreachable!(),
1911        }
1912    }
1913    Ok(())
1914}
1915
1916#[cfg(feature = "python")]
1917#[pyfunction(name = "adosc")]
1918#[pyo3(signature = (high, low, close, volume, short_period, long_period, kernel=None))]
1919pub fn adosc_py<'py>(
1920    py: Python<'py>,
1921    high: numpy::PyReadonlyArray1<'py, f64>,
1922    low: numpy::PyReadonlyArray1<'py, f64>,
1923    close: numpy::PyReadonlyArray1<'py, f64>,
1924    volume: numpy::PyReadonlyArray1<'py, f64>,
1925    short_period: usize,
1926    long_period: usize,
1927    kernel: Option<&str>,
1928) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
1929    use numpy::{IntoPyArray, PyArrayMethods};
1930
1931    let high_slice = high.as_slice()?;
1932    let low_slice = low.as_slice()?;
1933    let close_slice = close.as_slice()?;
1934    let volume_slice = volume.as_slice()?;
1935
1936    let len = close_slice.len();
1937    if high_slice.len() != len || low_slice.len() != len || volume_slice.len() != len {
1938        return Err(PyValueError::new_err(format!(
1939            "All input arrays must have the same length. Got high={}, low={}, close={}, volume={}",
1940            high_slice.len(),
1941            low_slice.len(),
1942            close_slice.len(),
1943            volume_slice.len()
1944        )));
1945    }
1946
1947    let kern = validate_kernel(kernel, false)?;
1948
1949    let params = AdoscParams {
1950        short_period: Some(short_period),
1951        long_period: Some(long_period),
1952    };
1953    let adosc_in =
1954        AdoscInput::from_slices(high_slice, low_slice, close_slice, volume_slice, params);
1955
1956    let result_vec: Vec<f64> = py
1957        .allow_threads(|| adosc_with_kernel(&adosc_in, kern).map(|o| o.values))
1958        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1959
1960    Ok(result_vec.into_pyarray(py))
1961}
1962
1963#[cfg(feature = "python")]
1964#[pyclass(name = "AdoscStream")]
1965pub struct AdoscStreamPy {
1966    stream: AdoscStream,
1967}
1968
1969#[cfg(feature = "python")]
1970#[pymethods]
1971impl AdoscStreamPy {
1972    #[new]
1973    fn new(short_period: usize, long_period: usize) -> PyResult<Self> {
1974        let params = AdoscParams {
1975            short_period: Some(short_period),
1976            long_period: Some(long_period),
1977        };
1978        let stream =
1979            AdoscStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1980        Ok(AdoscStreamPy { stream })
1981    }
1982
1983    fn update(&mut self, high: f64, low: f64, close: f64, volume: f64) -> f64 {
1984        self.stream.update(high, low, close, volume)
1985    }
1986}
1987
1988#[cfg(all(feature = "python", feature = "cuda"))]
1989#[pyclass(module = "ta_indicators.cuda", unsendable)]
1990pub struct DeviceArrayF32AdoscPy {
1991    pub(crate) inner: Option<crate::cuda::oscillators::adosc_wrapper::DeviceArrayF32Adosc>,
1992}
1993
1994#[cfg(all(feature = "python", feature = "cuda"))]
1995#[pymethods]
1996impl DeviceArrayF32AdoscPy {
1997    #[getter]
1998    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1999        let inner = self
2000            .inner
2001            .as_ref()
2002            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2003        let d = PyDict::new(py);
2004        d.set_item("shape", (inner.rows, inner.cols))?;
2005        d.set_item("typestr", "<f4")?;
2006        d.set_item(
2007            "strides",
2008            (
2009                inner.cols * std::mem::size_of::<f32>(),
2010                std::mem::size_of::<f32>(),
2011            ),
2012        )?;
2013        d.set_item("data", (inner.device_ptr() as usize, false))?;
2014
2015        d.set_item("version", 3)?;
2016        Ok(d)
2017    }
2018
2019    fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
2020        let inner = self
2021            .inner
2022            .as_ref()
2023            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2024
2025        Ok((2, inner.device_id as i32))
2026    }
2027
2028    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
2029    fn __dlpack__<'py>(
2030        &mut self,
2031        py: Python<'py>,
2032        stream: Option<pyo3::PyObject>,
2033        max_version: Option<pyo3::PyObject>,
2034        dl_device: Option<pyo3::PyObject>,
2035        copy: Option<pyo3::PyObject>,
2036    ) -> PyResult<PyObject> {
2037        use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2038
2039        if let Some(obj) = &stream {
2040            if let Ok(i) = obj.extract::<i64>(py) {
2041                if i == 0 {
2042                    return Err(PyValueError::new_err(
2043                        "__dlpack__: stream 0 is disallowed for CUDA",
2044                    ));
2045                }
2046            }
2047        }
2048
2049        let (kdl, alloc_dev) = self.__dlpack_device__()?;
2050        if let Some(dev_obj) = dl_device.as_ref() {
2051            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2052                if dev_ty != kdl || dev_id != alloc_dev {
2053                    let wants_copy = copy
2054                        .as_ref()
2055                        .and_then(|c| c.extract::<bool>(py).ok())
2056                        .unwrap_or(false);
2057                    if wants_copy {
2058                        return Err(PyValueError::new_err(
2059                            "device copy not implemented for __dlpack__",
2060                        ));
2061                    } else {
2062                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2063                    }
2064                }
2065            }
2066        }
2067
2068        let inner = self
2069            .inner
2070            .take()
2071            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2072        let rows = inner.rows;
2073        let cols = inner.cols;
2074        let device_id = inner.device_id as i32;
2075        let buf = inner.buf;
2076
2077        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2078
2079        export_f32_cuda_dlpack_2d(py, buf, rows, cols, device_id, max_version_bound)
2080    }
2081}
2082
2083#[cfg(feature = "python")]
2084#[pyfunction(name = "adosc_batch")]
2085#[pyo3(signature = (high, low, close, volume, short_period_range, long_period_range, kernel=None))]
2086pub fn adosc_batch_py<'py>(
2087    py: Python<'py>,
2088    high: numpy::PyReadonlyArray1<'py, f64>,
2089    low: numpy::PyReadonlyArray1<'py, f64>,
2090    close: numpy::PyReadonlyArray1<'py, f64>,
2091    volume: numpy::PyReadonlyArray1<'py, f64>,
2092    short_period_range: (usize, usize, usize),
2093    long_period_range: (usize, usize, usize),
2094    kernel: Option<&str>,
2095) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2096    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2097    use pyo3::types::PyDict;
2098
2099    let high_slice = high.as_slice()?;
2100    let low_slice = low.as_slice()?;
2101    let close_slice = close.as_slice()?;
2102    let volume_slice = volume.as_slice()?;
2103
2104    let len = close_slice.len();
2105    if high_slice.len() != len || low_slice.len() != len || volume_slice.len() != len {
2106        return Err(PyValueError::new_err(format!(
2107            "All input arrays must have the same length. Got high={}, low={}, close={}, volume={}",
2108            high_slice.len(),
2109            low_slice.len(),
2110            close_slice.len(),
2111            volume_slice.len()
2112        )));
2113    }
2114
2115    let sweep = AdoscBatchRange {
2116        short_period: short_period_range,
2117        long_period: long_period_range,
2118    };
2119
2120    let combos = expand_grid_checked(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2121    let rows = combos.len();
2122    let cols = len;
2123
2124    let expected = rows
2125        .checked_mul(cols)
2126        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
2127    let out_arr = unsafe { PyArray1::<f64>::new(py, [expected], false) };
2128    let slice_out = unsafe { out_arr.as_slice_mut()? };
2129
2130    let kern = validate_kernel(kernel, true)?;
2131
2132    let combos = py
2133        .allow_threads(|| -> Result<Vec<AdoscParams>, AdoscError> {
2134            let kernel = match kern {
2135                Kernel::Auto => detect_best_batch_kernel(),
2136                k => k,
2137            };
2138            let simd = match kernel {
2139                Kernel::Avx512Batch => Kernel::Avx512,
2140                Kernel::Avx2Batch => Kernel::Avx2,
2141                Kernel::ScalarBatch => Kernel::Scalar,
2142                _ => unreachable!(),
2143            };
2144
2145            adosc_batch_inner_into(
2146                high_slice,
2147                low_slice,
2148                close_slice,
2149                volume_slice,
2150                &sweep,
2151                simd,
2152                true,
2153                slice_out,
2154            )
2155        })
2156        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2157
2158    let dict = PyDict::new(py);
2159    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2160    dict.set_item(
2161        "shorts",
2162        combos
2163            .iter()
2164            .map(|p| p.short_period.unwrap() as u64)
2165            .collect::<Vec<_>>()
2166            .into_pyarray(py),
2167    )?;
2168    dict.set_item(
2169        "longs",
2170        combos
2171            .iter()
2172            .map(|p| p.long_period.unwrap() as u64)
2173            .collect::<Vec<_>>()
2174            .into_pyarray(py),
2175    )?;
2176
2177    Ok(dict)
2178}
2179
2180#[cfg(all(feature = "python", feature = "cuda"))]
2181#[pyfunction(name = "adosc_cuda_batch_dev")]
2182#[pyo3(signature = (high, low, close, volume, short_period_range, long_period_range, device_id=0))]
2183pub fn adosc_cuda_batch_dev_py(
2184    py: Python<'_>,
2185    high: numpy::PyReadonlyArray1<'_, f32>,
2186    low: numpy::PyReadonlyArray1<'_, f32>,
2187    close: numpy::PyReadonlyArray1<'_, f32>,
2188    volume: numpy::PyReadonlyArray1<'_, f32>,
2189    short_period_range: (usize, usize, usize),
2190    long_period_range: (usize, usize, usize),
2191    device_id: usize,
2192) -> PyResult<DeviceArrayF32AdoscPy> {
2193    use crate::cuda::cuda_available;
2194    if !cuda_available() {
2195        return Err(PyValueError::new_err("CUDA not available"));
2196    }
2197    let high_slice = high.as_slice()?;
2198    let low_slice = low.as_slice()?;
2199    let close_slice = close.as_slice()?;
2200    let volume_slice = volume.as_slice()?;
2201    let len = close_slice.len();
2202    if high_slice.len() != len || low_slice.len() != len || volume_slice.len() != len {
2203        return Err(PyValueError::new_err("mismatched input lengths"));
2204    }
2205    let sweep = AdoscBatchRange {
2206        short_period: short_period_range,
2207        long_period: long_period_range,
2208    };
2209    let inner = py.allow_threads(|| {
2210        let cuda = CudaAdosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2211        cuda.adosc_batch_dev(high_slice, low_slice, close_slice, volume_slice, &sweep)
2212            .map_err(|e| PyValueError::new_err(e.to_string()))
2213    })?;
2214    Ok(DeviceArrayF32AdoscPy { inner: Some(inner) })
2215}
2216
2217#[cfg(all(feature = "python", feature = "cuda"))]
2218#[pyfunction(name = "adosc_cuda_many_series_one_param_dev")]
2219#[pyo3(signature = (high_tm, low_tm, close_tm, volume_tm, cols, rows, short_period, long_period, device_id=0))]
2220pub fn adosc_cuda_many_series_one_param_dev_py(
2221    py: Python<'_>,
2222    high_tm: numpy::PyReadonlyArray1<'_, f32>,
2223    low_tm: numpy::PyReadonlyArray1<'_, f32>,
2224    close_tm: numpy::PyReadonlyArray1<'_, f32>,
2225    volume_tm: numpy::PyReadonlyArray1<'_, f32>,
2226    cols: usize,
2227    rows: usize,
2228    short_period: usize,
2229    long_period: usize,
2230    device_id: usize,
2231) -> PyResult<DeviceArrayF32AdoscPy> {
2232    use crate::cuda::cuda_available;
2233    if !cuda_available() {
2234        return Err(PyValueError::new_err("CUDA not available"));
2235    }
2236    let high_slice = high_tm.as_slice()?;
2237    let low_slice = low_tm.as_slice()?;
2238    let close_slice = close_tm.as_slice()?;
2239    let volume_slice = volume_tm.as_slice()?;
2240    let expected = cols
2241        .checked_mul(rows)
2242        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
2243    if high_slice.len() != expected
2244        || low_slice.len() != expected
2245        || close_slice.len() != expected
2246        || volume_slice.len() != expected
2247    {
2248        return Err(PyValueError::new_err("time-major input lengths mismatch"));
2249    }
2250    let inner = py.allow_threads(|| {
2251        let cuda = CudaAdosc::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2252        cuda.adosc_many_series_one_param_time_major_dev(
2253            high_slice,
2254            low_slice,
2255            close_slice,
2256            volume_slice,
2257            cols,
2258            rows,
2259            short_period,
2260            long_period,
2261        )
2262        .map_err(|e| PyValueError::new_err(e.to_string()))
2263    })?;
2264    Ok(DeviceArrayF32AdoscPy { inner: Some(inner) })
2265}
2266
2267#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2268#[wasm_bindgen]
2269pub fn adosc_js(
2270    high: &[f64],
2271    low: &[f64],
2272    close: &[f64],
2273    volume: &[f64],
2274    short_period: usize,
2275    long_period: usize,
2276) -> Result<Vec<f64>, JsValue> {
2277    let params = AdoscParams {
2278        short_period: Some(short_period),
2279        long_period: Some(long_period),
2280    };
2281    let input = AdoscInput::from_slices(high, low, close, volume, params);
2282
2283    let mut output = vec![0.0; high.len()];
2284    adosc_into_slice(&mut output, &input, Kernel::Auto)
2285        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2286
2287    Ok(output)
2288}
2289
2290#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2291#[wasm_bindgen]
2292pub fn adosc_batch_js(
2293    high: &[f64],
2294    low: &[f64],
2295    close: &[f64],
2296    volume: &[f64],
2297    short_period_start: usize,
2298    short_period_end: usize,
2299    short_period_step: usize,
2300    long_period_start: usize,
2301    long_period_end: usize,
2302    long_period_step: usize,
2303) -> Result<Vec<f64>, JsValue> {
2304    let sweep = AdoscBatchRange {
2305        short_period: (short_period_start, short_period_end, short_period_step),
2306        long_period: (long_period_start, long_period_end, long_period_step),
2307    };
2308
2309    adosc_batch_inner(high, low, close, volume, &sweep, Kernel::Scalar, false)
2310        .map(|output| output.values)
2311        .map_err(|e| JsValue::from_str(&e.to_string()))
2312}
2313
2314#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2315#[wasm_bindgen]
2316pub fn adosc_batch_metadata_js(
2317    short_period_start: usize,
2318    short_period_end: usize,
2319    short_period_step: usize,
2320    long_period_start: usize,
2321    long_period_end: usize,
2322    long_period_step: usize,
2323) -> Result<Vec<f64>, JsValue> {
2324    let sweep = AdoscBatchRange {
2325        short_period: (short_period_start, short_period_end, short_period_step),
2326        long_period: (long_period_start, long_period_end, long_period_step),
2327    };
2328
2329    let combos = expand_grid(&sweep);
2330    let mut metadata = Vec::with_capacity(combos.len() * 2);
2331
2332    for combo in combos {
2333        metadata.push(combo.short_period.unwrap() as f64);
2334        metadata.push(combo.long_period.unwrap() as f64);
2335    }
2336
2337    Ok(metadata)
2338}
2339
2340#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2341#[derive(Serialize, Deserialize)]
2342pub struct AdoscBatchConfig {
2343    pub short_period_range: (usize, usize, usize),
2344    pub long_period_range: (usize, usize, usize),
2345}
2346
2347#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2348#[derive(Serialize, Deserialize)]
2349pub struct AdoscBatchJsOutput {
2350    pub values: Vec<f64>,
2351    pub combos: Vec<AdoscParams>,
2352    pub rows: usize,
2353    pub cols: usize,
2354}
2355
2356#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2357#[wasm_bindgen(js_name = adosc_batch)]
2358pub fn adosc_batch_unified_js(
2359    high: &[f64],
2360    low: &[f64],
2361    close: &[f64],
2362    volume: &[f64],
2363    config: JsValue,
2364) -> Result<JsValue, JsValue> {
2365    let config: AdoscBatchConfig = serde_wasm_bindgen::from_value(config)
2366        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2367
2368    let sweep = AdoscBatchRange {
2369        short_period: config.short_period_range,
2370        long_period: config.long_period_range,
2371    };
2372
2373    let output = adosc_batch_inner(high, low, close, volume, &sweep, Kernel::Scalar, false)
2374        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2375
2376    let js_output = AdoscBatchJsOutput {
2377        values: output.values,
2378        combos: output.combos,
2379        rows: output.rows,
2380        cols: output.cols,
2381    };
2382
2383    serde_wasm_bindgen::to_value(&js_output)
2384        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2385}
2386
2387#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2388#[wasm_bindgen]
2389pub fn adosc_alloc(len: usize) -> *mut f64 {
2390    let mut vec = Vec::<f64>::with_capacity(len);
2391    let ptr = vec.as_mut_ptr();
2392    std::mem::forget(vec);
2393    ptr
2394}
2395
2396#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2397#[wasm_bindgen]
2398pub fn adosc_free(ptr: *mut f64, len: usize) {
2399    if !ptr.is_null() {
2400        unsafe {
2401            let _ = Vec::from_raw_parts(ptr, len, len);
2402        }
2403    }
2404}
2405
2406#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2407#[wasm_bindgen]
2408pub fn adosc_into(
2409    high_ptr: *const f64,
2410    low_ptr: *const f64,
2411    close_ptr: *const f64,
2412    volume_ptr: *const f64,
2413    out_ptr: *mut f64,
2414    len: usize,
2415    short_period: usize,
2416    long_period: usize,
2417) -> Result<(), JsValue> {
2418    if high_ptr.is_null()
2419        || low_ptr.is_null()
2420        || close_ptr.is_null()
2421        || volume_ptr.is_null()
2422        || out_ptr.is_null()
2423    {
2424        return Err(JsValue::from_str("Null pointer provided"));
2425    }
2426
2427    unsafe {
2428        let high = std::slice::from_raw_parts(high_ptr, len);
2429        let low = std::slice::from_raw_parts(low_ptr, len);
2430        let close = std::slice::from_raw_parts(close_ptr, len);
2431        let volume = std::slice::from_raw_parts(volume_ptr, len);
2432
2433        let params = AdoscParams {
2434            short_period: Some(short_period),
2435            long_period: Some(long_period),
2436        };
2437        let input = AdoscInput::from_slices(high, low, close, volume, params);
2438
2439        if out_ptr as *const f64 == high_ptr
2440            || out_ptr as *const f64 == low_ptr
2441            || out_ptr as *const f64 == close_ptr
2442            || out_ptr as *const f64 == volume_ptr
2443        {
2444            let mut temp = vec![0.0; len];
2445            adosc_into_slice(&mut temp, &input, Kernel::Auto)
2446                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2447            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2448            out.copy_from_slice(&temp);
2449        } else {
2450            let out = std::slice::from_raw_parts_mut(out_ptr, len);
2451            adosc_into_slice(out, &input, Kernel::Auto)
2452                .map_err(|e| JsValue::from_str(&e.to_string()))?;
2453        }
2454
2455        Ok(())
2456    }
2457}
2458
2459#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2460#[wasm_bindgen]
2461pub fn adosc_batch_into(
2462    high_ptr: *const f64,
2463    low_ptr: *const f64,
2464    close_ptr: *const f64,
2465    volume_ptr: *const f64,
2466    out_ptr: *mut f64,
2467    len: usize,
2468    short_period_start: usize,
2469    short_period_end: usize,
2470    short_period_step: usize,
2471    long_period_start: usize,
2472    long_period_end: usize,
2473    long_period_step: usize,
2474) -> Result<usize, JsValue> {
2475    if high_ptr.is_null()
2476        || low_ptr.is_null()
2477        || close_ptr.is_null()
2478        || volume_ptr.is_null()
2479        || out_ptr.is_null()
2480    {
2481        return Err(JsValue::from_str("null pointer passed to adosc_batch_into"));
2482    }
2483
2484    unsafe {
2485        let high = std::slice::from_raw_parts(high_ptr, len);
2486        let low = std::slice::from_raw_parts(low_ptr, len);
2487        let close = std::slice::from_raw_parts(close_ptr, len);
2488        let volume = std::slice::from_raw_parts(volume_ptr, len);
2489
2490        let sweep = AdoscBatchRange {
2491            short_period: (short_period_start, short_period_end, short_period_step),
2492            long_period: (long_period_start, long_period_end, long_period_step),
2493        };
2494
2495        let combos = expand_grid_checked(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2496        let rows = combos.len();
2497        let cols = len;
2498        let expected = rows
2499            .checked_mul(cols)
2500            .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
2501        let out = std::slice::from_raw_parts_mut(out_ptr, expected);
2502
2503        adosc_batch_inner_into(high, low, close, volume, &sweep, Kernel::Scalar, false, out)
2504            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2505
2506        Ok(rows)
2507    }
2508}