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}