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