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