1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::mem::{ManuallyDrop, MaybeUninit};
27use thiserror::Error;
28
29const DEFAULT_LENGTH: usize = 50;
30const DEFAULT_MULT: f64 = 2.0;
31const ATR_FALLBACK_PERIOD: usize = 200;
32const ATR_PRIMARY_PERIOD: usize = 2000;
33const ZERO_EPS: f64 = 1e-12;
34
35#[derive(Debug, Clone)]
36pub enum RangeOscillatorData<'a> {
37 Candles {
38 candles: &'a Candles,
39 },
40 Slices {
41 high: &'a [f64],
42 low: &'a [f64],
43 close: &'a [f64],
44 },
45}
46
47#[derive(Debug, Clone)]
48pub struct RangeOscillatorOutput {
49 pub oscillator: Vec<f64>,
50 pub ma: Vec<f64>,
51 pub upper_band: Vec<f64>,
52 pub lower_band: Vec<f64>,
53 pub range_width: Vec<f64>,
54 pub in_range: Vec<f64>,
55 pub trend: Vec<f64>,
56 pub break_up: Vec<f64>,
57 pub break_down: Vec<f64>,
58}
59
60#[derive(Debug, Clone)]
61#[cfg_attr(
62 all(target_arch = "wasm32", feature = "wasm"),
63 derive(Serialize, Deserialize)
64)]
65pub struct RangeOscillatorParams {
66 pub length: Option<usize>,
67 pub mult: Option<f64>,
68}
69
70impl Default for RangeOscillatorParams {
71 fn default() -> Self {
72 Self {
73 length: Some(DEFAULT_LENGTH),
74 mult: Some(DEFAULT_MULT),
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
80pub struct RangeOscillatorInput<'a> {
81 pub data: RangeOscillatorData<'a>,
82 pub params: RangeOscillatorParams,
83}
84
85impl<'a> RangeOscillatorInput<'a> {
86 #[inline]
87 pub fn from_candles(candles: &'a Candles, params: RangeOscillatorParams) -> Self {
88 Self {
89 data: RangeOscillatorData::Candles { candles },
90 params,
91 }
92 }
93
94 #[inline]
95 pub fn from_slices(
96 high: &'a [f64],
97 low: &'a [f64],
98 close: &'a [f64],
99 params: RangeOscillatorParams,
100 ) -> Self {
101 Self {
102 data: RangeOscillatorData::Slices { high, low, close },
103 params,
104 }
105 }
106
107 #[inline]
108 pub fn with_default_candles(candles: &'a Candles) -> Self {
109 Self::from_candles(candles, RangeOscillatorParams::default())
110 }
111
112 #[inline(always)]
113 pub fn get_length(&self) -> usize {
114 self.params.length.unwrap_or(DEFAULT_LENGTH)
115 }
116
117 #[inline(always)]
118 pub fn get_mult(&self) -> f64 {
119 self.params.mult.unwrap_or(DEFAULT_MULT)
120 }
121}
122
123#[derive(Clone, Debug)]
124pub struct RangeOscillatorBuilder {
125 length: Option<usize>,
126 mult: Option<f64>,
127 kernel: Kernel,
128}
129
130impl Default for RangeOscillatorBuilder {
131 fn default() -> Self {
132 Self {
133 length: None,
134 mult: None,
135 kernel: Kernel::Auto,
136 }
137 }
138}
139
140impl RangeOscillatorBuilder {
141 #[inline(always)]
142 pub fn new() -> Self {
143 Self::default()
144 }
145
146 #[inline(always)]
147 pub fn length(mut self, value: usize) -> Self {
148 self.length = Some(value);
149 self
150 }
151
152 #[inline(always)]
153 pub fn mult(mut self, value: f64) -> Self {
154 self.mult = Some(value);
155 self
156 }
157
158 #[inline(always)]
159 pub fn kernel(mut self, value: Kernel) -> Self {
160 self.kernel = value;
161 self
162 }
163
164 #[inline(always)]
165 pub fn apply(self, candles: &Candles) -> Result<RangeOscillatorOutput, RangeOscillatorError> {
166 let input = RangeOscillatorInput::from_candles(
167 candles,
168 RangeOscillatorParams {
169 length: self.length,
170 mult: self.mult,
171 },
172 );
173 range_oscillator_with_kernel(&input, self.kernel)
174 }
175
176 #[inline(always)]
177 pub fn apply_slices(
178 self,
179 high: &[f64],
180 low: &[f64],
181 close: &[f64],
182 ) -> Result<RangeOscillatorOutput, RangeOscillatorError> {
183 let input = RangeOscillatorInput::from_slices(
184 high,
185 low,
186 close,
187 RangeOscillatorParams {
188 length: self.length,
189 mult: self.mult,
190 },
191 );
192 range_oscillator_with_kernel(&input, self.kernel)
193 }
194
195 #[inline(always)]
196 pub fn into_stream(self) -> Result<RangeOscillatorStream, RangeOscillatorError> {
197 RangeOscillatorStream::try_new(RangeOscillatorParams {
198 length: self.length,
199 mult: self.mult,
200 })
201 }
202}
203
204#[derive(Debug, Error)]
205pub enum RangeOscillatorError {
206 #[error("range_oscillator: input data slice is empty")]
207 EmptyInputData,
208 #[error("range_oscillator: data length mismatch: high={high}, low={low}, close={close}")]
209 DataLengthMismatch {
210 high: usize,
211 low: usize,
212 close: usize,
213 },
214 #[error("range_oscillator: all values are NaN")]
215 AllValuesNaN,
216 #[error("range_oscillator: invalid length: length = {length}, data length = {data_len}")]
217 InvalidLength { length: usize, data_len: usize },
218 #[error("range_oscillator: invalid mult: {mult}")]
219 InvalidMult { mult: f64 },
220 #[error("range_oscillator: not enough valid data: needed = {needed}, valid = {valid}")]
221 NotEnoughValidData { needed: usize, valid: usize },
222 #[error("range_oscillator: output length mismatch: expected {expected}, got {got}")]
223 OutputLengthMismatch { expected: usize, got: usize },
224 #[error("range_oscillator: invalid range: start={start}, end={end}, step={step}")]
225 InvalidRange {
226 start: String,
227 end: String,
228 step: String,
229 },
230 #[error("range_oscillator: invalid kernel for batch: {0:?}")]
231 InvalidKernelForBatch(Kernel),
232}
233
234#[derive(Clone, Debug)]
235struct PreparedInput<'a> {
236 high: &'a [f64],
237 low: &'a [f64],
238 close: &'a [f64],
239 len: usize,
240 length: usize,
241 mult: f64,
242 warmup: usize,
243}
244
245#[derive(Clone, Debug)]
246struct AtrState {
247 period: usize,
248 count: usize,
249 sum: f64,
250 value: Option<f64>,
251 prev_close: Option<f64>,
252}
253
254impl AtrState {
255 #[inline(always)]
256 fn new(period: usize) -> Self {
257 Self {
258 period,
259 count: 0,
260 sum: 0.0,
261 value: None,
262 prev_close: None,
263 }
264 }
265
266 #[inline(always)]
267 fn reset(&mut self) {
268 self.count = 0;
269 self.sum = 0.0;
270 self.value = None;
271 self.prev_close = None;
272 }
273
274 #[inline(always)]
275 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
276 let tr = if let Some(prev_close) = self.prev_close {
277 let hl = high - low;
278 let hc = (high - prev_close).abs();
279 let lc = (low - prev_close).abs();
280 hl.max(hc).max(lc)
281 } else {
282 high - low
283 };
284 self.prev_close = Some(close);
285
286 if let Some(prev) = self.value {
287 let next = (prev * (self.period as f64 - 1.0) + tr) / self.period as f64;
288 self.value = Some(next);
289 Some(next)
290 } else {
291 self.count += 1;
292 self.sum += tr;
293 if self.count == self.period {
294 let seeded = self.sum / self.period as f64;
295 self.value = Some(seeded);
296 Some(seeded)
297 } else {
298 None
299 }
300 }
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq)]
305pub struct RangeOscillatorStreamOutput {
306 pub oscillator: f64,
307 pub ma: f64,
308 pub upper_band: f64,
309 pub lower_band: f64,
310 pub range_width: f64,
311 pub in_range: f64,
312 pub trend: f64,
313 pub break_up: f64,
314 pub break_down: f64,
315}
316
317#[derive(Debug, Clone)]
318pub struct RangeOscillatorStream {
319 length: usize,
320 mult: f64,
321 atr_fallback: AtrState,
322 atr_primary: AtrState,
323 closes: VecDeque<f64>,
324 trend: f64,
325}
326
327#[inline]
328pub fn range_oscillator(
329 input: &RangeOscillatorInput<'_>,
330) -> Result<RangeOscillatorOutput, RangeOscillatorError> {
331 range_oscillator_with_kernel(input, Kernel::Auto)
332}
333
334#[inline]
335pub fn range_oscillator_with_kernel(
336 input: &RangeOscillatorInput<'_>,
337 kernel: Kernel,
338) -> Result<RangeOscillatorOutput, RangeOscillatorError> {
339 let prepared = prepare_input(input, kernel)?;
340 let mut oscillator = alloc_with_nan_prefix(prepared.len, prepared.warmup);
341 let mut ma = alloc_with_nan_prefix(prepared.len, prepared.warmup);
342 let mut upper_band = alloc_with_nan_prefix(prepared.len, prepared.warmup);
343 let mut lower_band = alloc_with_nan_prefix(prepared.len, prepared.warmup);
344 let mut range_width = alloc_with_nan_prefix(prepared.len, prepared.warmup);
345 let mut in_range = alloc_with_nan_prefix(prepared.len, prepared.warmup);
346 let mut trend = alloc_with_nan_prefix(prepared.len, prepared.warmup);
347 let mut break_up = alloc_with_nan_prefix(prepared.len, prepared.warmup);
348 let mut break_down = alloc_with_nan_prefix(prepared.len, prepared.warmup);
349
350 range_oscillator_into_slices(
351 input,
352 kernel,
353 &mut oscillator,
354 &mut ma,
355 &mut upper_band,
356 &mut lower_band,
357 &mut range_width,
358 &mut in_range,
359 &mut trend,
360 &mut break_up,
361 &mut break_down,
362 )?;
363
364 Ok(RangeOscillatorOutput {
365 oscillator,
366 ma,
367 upper_band,
368 lower_band,
369 range_width,
370 in_range,
371 trend,
372 break_up,
373 break_down,
374 })
375}
376
377#[inline]
378pub fn range_oscillator_into(
379 input: &RangeOscillatorInput<'_>,
380 oscillator: &mut [f64],
381 ma: &mut [f64],
382 upper_band: &mut [f64],
383 lower_band: &mut [f64],
384 range_width: &mut [f64],
385 in_range: &mut [f64],
386 trend: &mut [f64],
387 break_up: &mut [f64],
388 break_down: &mut [f64],
389) -> Result<(), RangeOscillatorError> {
390 range_oscillator_into_slices(
391 input,
392 Kernel::Auto,
393 oscillator,
394 ma,
395 upper_band,
396 lower_band,
397 range_width,
398 in_range,
399 trend,
400 break_up,
401 break_down,
402 )
403}
404
405#[allow(clippy::too_many_arguments)]
406#[inline]
407pub fn range_oscillator_into_slices(
408 input: &RangeOscillatorInput<'_>,
409 kernel: Kernel,
410 oscillator: &mut [f64],
411 ma: &mut [f64],
412 upper_band: &mut [f64],
413 lower_band: &mut [f64],
414 range_width: &mut [f64],
415 in_range: &mut [f64],
416 trend: &mut [f64],
417 break_up: &mut [f64],
418 break_down: &mut [f64],
419) -> Result<(), RangeOscillatorError> {
420 let prepared = prepare_input(input, kernel)?;
421 let got = *[
422 oscillator.len(),
423 ma.len(),
424 upper_band.len(),
425 lower_band.len(),
426 range_width.len(),
427 in_range.len(),
428 trend.len(),
429 break_up.len(),
430 break_down.len(),
431 ]
432 .iter()
433 .min()
434 .unwrap_or(&0);
435 if oscillator.len() != prepared.len
436 || ma.len() != prepared.len
437 || upper_band.len() != prepared.len
438 || lower_band.len() != prepared.len
439 || range_width.len() != prepared.len
440 || in_range.len() != prepared.len
441 || trend.len() != prepared.len
442 || break_up.len() != prepared.len
443 || break_down.len() != prepared.len
444 {
445 return Err(RangeOscillatorError::OutputLengthMismatch {
446 expected: prepared.len,
447 got,
448 });
449 }
450
451 compute_into_slices(
452 &prepared,
453 oscillator,
454 ma,
455 upper_band,
456 lower_band,
457 range_width,
458 in_range,
459 trend,
460 break_up,
461 break_down,
462 )
463}
464
465#[inline]
466fn resolve_data<'a>(
467 input: &'a RangeOscillatorInput<'a>,
468) -> Result<(&'a [f64], &'a [f64], &'a [f64]), RangeOscillatorError> {
469 match &input.data {
470 RangeOscillatorData::Candles { candles } => Ok((
471 candles.high.as_slice(),
472 candles.low.as_slice(),
473 candles.close.as_slice(),
474 )),
475 RangeOscillatorData::Slices { high, low, close } => {
476 if high.len() != low.len() || high.len() != close.len() {
477 return Err(RangeOscillatorError::DataLengthMismatch {
478 high: high.len(),
479 low: low.len(),
480 close: close.len(),
481 });
482 }
483 Ok((high, low, close))
484 }
485 }
486}
487
488#[inline]
489fn prepare_input<'a>(
490 input: &'a RangeOscillatorInput<'a>,
491 kernel: Kernel,
492) -> Result<PreparedInput<'a>, RangeOscillatorError> {
493 let (high, low, close) = resolve_data(input)?;
494 let len = close.len();
495 if len == 0 {
496 return Err(RangeOscillatorError::EmptyInputData);
497 }
498 let first = (0..len)
499 .find(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
500 .ok_or(RangeOscillatorError::AllValuesNaN)?;
501
502 let length = input.get_length();
503 let mult = input.get_mult();
504
505 if length == 0 || length >= len {
506 return Err(RangeOscillatorError::InvalidLength {
507 length,
508 data_len: len,
509 });
510 }
511 if !mult.is_finite() || mult < 0.1 {
512 return Err(RangeOscillatorError::InvalidMult { mult });
513 }
514
515 let valid = (first..len)
516 .filter(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
517 .count();
518 let needed = (length + 1).max(ATR_FALLBACK_PERIOD);
519 if valid < needed {
520 return Err(RangeOscillatorError::NotEnoughValidData { needed, valid });
521 }
522
523 let _chosen = match kernel {
524 Kernel::Auto => detect_best_kernel(),
525 value => value,
526 };
527
528 Ok(PreparedInput {
529 high,
530 low,
531 close,
532 len,
533 length,
534 mult,
535 warmup: first + length.max(ATR_FALLBACK_PERIOD - 1),
536 })
537}
538
539#[inline(always)]
540fn compute_weighted_ma(closes: &VecDeque<f64>, length: usize) -> Option<f64> {
541 if closes.len() < length + 1 {
542 return None;
543 }
544 let mut sum_weighted = 0.0;
545 let mut sum_weights = 0.0;
546 let last = closes.len() - 1;
547 for i in 0..length {
548 let curr = closes[last - i];
549 let prev = closes[last - i - 1];
550 if prev.abs() <= ZERO_EPS {
551 continue;
552 }
553 let delta = (curr - prev).abs();
554 let w = delta / prev;
555 sum_weighted += curr * w;
556 sum_weights += w;
557 }
558 if sum_weights.abs() <= ZERO_EPS {
559 None
560 } else {
561 Some(sum_weighted / sum_weights)
562 }
563}
564
565#[inline(always)]
566fn compute_point(
567 closes: &VecDeque<f64>,
568 current_close: f64,
569 range_width: f64,
570 trend_state: &mut f64,
571) -> Option<RangeOscillatorStreamOutput> {
572 let length = closes.len().saturating_sub(1);
573 let ma = compute_weighted_ma(closes, length)?;
574 let mut max_dist = 0.0;
575 let last = closes.len() - 1;
576 for i in 0..length {
577 let value = closes[last - i];
578 let dist = (value - ma).abs();
579 if dist > max_dist {
580 max_dist = dist;
581 }
582 }
583
584 if current_close > ma {
585 *trend_state = 1.0;
586 } else if current_close < ma {
587 *trend_state = -1.0;
588 }
589
590 let upper_band = ma + range_width;
591 let lower_band = ma - range_width;
592 let break_up = if current_close > upper_band { 1.0 } else { 0.0 };
593 let break_down = if current_close < lower_band { 1.0 } else { 0.0 };
594 let oscillator = if range_width.abs() <= ZERO_EPS {
595 f64::NAN
596 } else {
597 100.0 * (current_close - ma) / range_width
598 };
599
600 Some(RangeOscillatorStreamOutput {
601 oscillator,
602 ma,
603 upper_band,
604 lower_band,
605 range_width,
606 in_range: if max_dist <= range_width { 1.0 } else { 0.0 },
607 trend: *trend_state,
608 break_up,
609 break_down,
610 })
611}
612
613#[allow(clippy::too_many_arguments)]
614#[inline(always)]
615fn compute_into_slices(
616 prepared: &PreparedInput<'_>,
617 dst_oscillator: &mut [f64],
618 dst_ma: &mut [f64],
619 dst_upper_band: &mut [f64],
620 dst_lower_band: &mut [f64],
621 dst_range_width: &mut [f64],
622 dst_in_range: &mut [f64],
623 dst_trend: &mut [f64],
624 dst_break_up: &mut [f64],
625 dst_break_down: &mut [f64],
626) -> Result<(), RangeOscillatorError> {
627 let got = *[
628 dst_oscillator.len(),
629 dst_ma.len(),
630 dst_upper_band.len(),
631 dst_lower_band.len(),
632 dst_range_width.len(),
633 dst_in_range.len(),
634 dst_trend.len(),
635 dst_break_up.len(),
636 dst_break_down.len(),
637 ]
638 .iter()
639 .min()
640 .unwrap_or(&0);
641 if dst_oscillator.len() != prepared.len
642 || dst_ma.len() != prepared.len
643 || dst_upper_band.len() != prepared.len
644 || dst_lower_band.len() != prepared.len
645 || dst_range_width.len() != prepared.len
646 || dst_in_range.len() != prepared.len
647 || dst_trend.len() != prepared.len
648 || dst_break_up.len() != prepared.len
649 || dst_break_down.len() != prepared.len
650 {
651 return Err(RangeOscillatorError::OutputLengthMismatch {
652 expected: prepared.len,
653 got,
654 });
655 }
656
657 dst_oscillator.fill(f64::NAN);
658 dst_ma.fill(f64::NAN);
659 dst_upper_band.fill(f64::NAN);
660 dst_lower_band.fill(f64::NAN);
661 dst_range_width.fill(f64::NAN);
662 dst_in_range.fill(f64::NAN);
663 dst_trend.fill(f64::NAN);
664 dst_break_up.fill(f64::NAN);
665 dst_break_down.fill(f64::NAN);
666
667 let mut atr_fallback = AtrState::new(ATR_FALLBACK_PERIOD);
668 let mut atr_primary = AtrState::new(ATR_PRIMARY_PERIOD);
669 let mut closes = VecDeque::with_capacity(prepared.length + 1);
670 let mut trend_state = 0.0;
671
672 for i in 0..prepared.len {
673 let high = prepared.high[i];
674 let low = prepared.low[i];
675 let close = prepared.close[i];
676 if !high.is_finite() || !low.is_finite() || !close.is_finite() {
677 atr_fallback.reset();
678 atr_primary.reset();
679 closes.clear();
680 trend_state = 0.0;
681 continue;
682 }
683
684 let atr200 = atr_fallback.update(high, low, close);
685 let atr2000 = atr_primary.update(high, low, close);
686 let atr_raw = atr2000.or(atr200);
687
688 if closes.len() == prepared.length + 1 {
689 closes.pop_front();
690 }
691 closes.push_back(close);
692
693 let Some(atr_raw) = atr_raw else {
694 continue;
695 };
696 if closes.len() < prepared.length + 1 {
697 continue;
698 }
699
700 let range_width = atr_raw * prepared.mult;
701 let Some(point) = compute_point(&closes, close, range_width, &mut trend_state) else {
702 continue;
703 };
704
705 dst_oscillator[i] = point.oscillator;
706 dst_ma[i] = point.ma;
707 dst_upper_band[i] = point.upper_band;
708 dst_lower_band[i] = point.lower_band;
709 dst_range_width[i] = point.range_width;
710 dst_in_range[i] = point.in_range;
711 dst_trend[i] = point.trend;
712 dst_break_up[i] = point.break_up;
713 dst_break_down[i] = point.break_down;
714 }
715
716 Ok(())
717}
718
719#[derive(Clone, Debug)]
720pub struct RangeOscillatorBatchRange {
721 pub length: (usize, usize, usize),
722 pub mult: (f64, f64, f64),
723}
724
725impl Default for RangeOscillatorBatchRange {
726 fn default() -> Self {
727 Self {
728 length: (DEFAULT_LENGTH, DEFAULT_LENGTH, 0),
729 mult: (DEFAULT_MULT, DEFAULT_MULT, 0.0),
730 }
731 }
732}
733
734#[derive(Clone, Debug)]
735pub struct RangeOscillatorBatchOutput {
736 pub oscillator: Vec<f64>,
737 pub ma: Vec<f64>,
738 pub upper_band: Vec<f64>,
739 pub lower_band: Vec<f64>,
740 pub range_width: Vec<f64>,
741 pub in_range: Vec<f64>,
742 pub trend: Vec<f64>,
743 pub break_up: Vec<f64>,
744 pub break_down: Vec<f64>,
745 pub combos: Vec<RangeOscillatorParams>,
746 pub rows: usize,
747 pub cols: usize,
748}
749
750#[derive(Clone, Debug)]
751pub struct RangeOscillatorBatchBuilder {
752 range: RangeOscillatorBatchRange,
753 kernel: Kernel,
754}
755
756impl Default for RangeOscillatorBatchBuilder {
757 fn default() -> Self {
758 Self {
759 range: RangeOscillatorBatchRange::default(),
760 kernel: Kernel::Auto,
761 }
762 }
763}
764
765impl RangeOscillatorBatchBuilder {
766 #[inline(always)]
767 pub fn new() -> Self {
768 Self::default()
769 }
770
771 #[inline(always)]
772 pub fn range(mut self, value: RangeOscillatorBatchRange) -> Self {
773 self.range = value;
774 self
775 }
776
777 #[inline(always)]
778 pub fn kernel(mut self, value: Kernel) -> Self {
779 self.kernel = value;
780 self
781 }
782
783 #[inline(always)]
784 pub fn apply(
785 self,
786 candles: &Candles,
787 ) -> Result<RangeOscillatorBatchOutput, RangeOscillatorError> {
788 self.apply_slices(
789 candles.high.as_slice(),
790 candles.low.as_slice(),
791 candles.close.as_slice(),
792 )
793 }
794
795 #[inline(always)]
796 pub fn apply_slices(
797 self,
798 high: &[f64],
799 low: &[f64],
800 close: &[f64],
801 ) -> Result<RangeOscillatorBatchOutput, RangeOscillatorError> {
802 range_oscillator_batch_with_kernel(high, low, close, &self.range, self.kernel)
803 }
804}
805
806fn axis_usize(
807 (start, end, step): (usize, usize, usize),
808) -> Result<Vec<usize>, RangeOscillatorError> {
809 if step == 0 || start == end {
810 return Ok(vec![start]);
811 }
812 let mut out = Vec::new();
813 if start <= end {
814 let mut current = start;
815 while current <= end {
816 out.push(current);
817 match current.checked_add(step) {
818 Some(next) => current = next,
819 None => break,
820 }
821 }
822 } else {
823 let mut current = start;
824 while current >= end {
825 out.push(current);
826 match current.checked_sub(step) {
827 Some(next) => current = next,
828 None => break,
829 }
830 if current < end {
831 break;
832 }
833 }
834 }
835 if out.is_empty() {
836 return Err(RangeOscillatorError::InvalidRange {
837 start: start.to_string(),
838 end: end.to_string(),
839 step: step.to_string(),
840 });
841 }
842 Ok(out)
843}
844
845fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, RangeOscillatorError> {
846 let eps = 1e-12;
847 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
848 return Err(RangeOscillatorError::InvalidRange {
849 start: start.to_string(),
850 end: end.to_string(),
851 step: step.to_string(),
852 });
853 }
854 if step.abs() < eps || (start - end).abs() < eps {
855 return Ok(vec![start]);
856 }
857 let mut out = Vec::new();
858 let dir = if end >= start { 1.0 } else { -1.0 };
859 let step_eff = dir * step.abs();
860 let mut current = start;
861 if dir > 0.0 {
862 while current <= end + eps {
863 out.push(current);
864 current += step_eff;
865 }
866 } else {
867 while current >= end - eps {
868 out.push(current);
869 current += step_eff;
870 }
871 }
872 if out.is_empty() {
873 return Err(RangeOscillatorError::InvalidRange {
874 start: start.to_string(),
875 end: end.to_string(),
876 step: step.to_string(),
877 });
878 }
879 Ok(out)
880}
881
882fn expand_grid(
883 range: &RangeOscillatorBatchRange,
884) -> Result<Vec<RangeOscillatorParams>, RangeOscillatorError> {
885 let lengths = axis_usize(range.length)?;
886 let mults = axis_f64(range.mult)?;
887 let total = lengths.len().checked_mul(mults.len()).ok_or_else(|| {
888 RangeOscillatorError::InvalidRange {
889 start: range.length.0.to_string(),
890 end: range.length.1.to_string(),
891 step: range.length.2.to_string(),
892 }
893 })?;
894
895 let mut out = Vec::with_capacity(total);
896 for &length in &lengths {
897 for &mult in &mults {
898 out.push(RangeOscillatorParams {
899 length: Some(length),
900 mult: Some(mult),
901 });
902 }
903 }
904 Ok(out)
905}
906
907#[inline]
908pub fn range_oscillator_batch_with_kernel(
909 high: &[f64],
910 low: &[f64],
911 close: &[f64],
912 range: &RangeOscillatorBatchRange,
913 kernel: Kernel,
914) -> Result<RangeOscillatorBatchOutput, RangeOscillatorError> {
915 if high.is_empty() || low.is_empty() || close.is_empty() {
916 return Err(RangeOscillatorError::EmptyInputData);
917 }
918 if high.len() != low.len() || high.len() != close.len() {
919 return Err(RangeOscillatorError::DataLengthMismatch {
920 high: high.len(),
921 low: low.len(),
922 close: close.len(),
923 });
924 }
925
926 let batch_kernel = match kernel {
927 Kernel::Auto => detect_best_batch_kernel(),
928 value if value.is_batch() => value,
929 _ => return Err(RangeOscillatorError::InvalidKernelForBatch(kernel)),
930 };
931 let single_kernel = batch_kernel.to_non_batch();
932 let combos = expand_grid(range)?;
933 let rows = combos.len();
934 let cols = close.len();
935
936 let first = (0..cols)
937 .find(|&i| high[i].is_finite() && low[i].is_finite() && close[i].is_finite())
938 .ok_or(RangeOscillatorError::AllValuesNaN)?;
939 let warmups: Vec<usize> = combos
940 .iter()
941 .map(|combo| {
942 first
943 + combo
944 .length
945 .unwrap_or(DEFAULT_LENGTH)
946 .max(ATR_FALLBACK_PERIOD - 1)
947 })
948 .collect();
949
950 let mut osc_mu = make_uninit_matrix(rows, cols);
951 let mut ma_mu = make_uninit_matrix(rows, cols);
952 let mut upper_mu = make_uninit_matrix(rows, cols);
953 let mut lower_mu = make_uninit_matrix(rows, cols);
954 let mut width_mu = make_uninit_matrix(rows, cols);
955 let mut in_range_mu = make_uninit_matrix(rows, cols);
956 let mut trend_mu = make_uninit_matrix(rows, cols);
957 let mut break_up_mu = make_uninit_matrix(rows, cols);
958 let mut break_down_mu = make_uninit_matrix(rows, cols);
959
960 init_matrix_prefixes(&mut osc_mu, cols, &warmups);
961 init_matrix_prefixes(&mut ma_mu, cols, &warmups);
962 init_matrix_prefixes(&mut upper_mu, cols, &warmups);
963 init_matrix_prefixes(&mut lower_mu, cols, &warmups);
964 init_matrix_prefixes(&mut width_mu, cols, &warmups);
965 init_matrix_prefixes(&mut in_range_mu, cols, &warmups);
966 init_matrix_prefixes(&mut trend_mu, cols, &warmups);
967 init_matrix_prefixes(&mut break_up_mu, cols, &warmups);
968 init_matrix_prefixes(&mut break_down_mu, cols, &warmups);
969
970 let mut osc_guard = ManuallyDrop::new(osc_mu);
971 let mut ma_guard = ManuallyDrop::new(ma_mu);
972 let mut upper_guard = ManuallyDrop::new(upper_mu);
973 let mut lower_guard = ManuallyDrop::new(lower_mu);
974 let mut width_guard = ManuallyDrop::new(width_mu);
975 let mut in_range_guard = ManuallyDrop::new(in_range_mu);
976 let mut trend_guard = ManuallyDrop::new(trend_mu);
977 let mut break_up_guard = ManuallyDrop::new(break_up_mu);
978 let mut break_down_guard = ManuallyDrop::new(break_down_mu);
979
980 let osc_all = unsafe { mu_slice_as_f64_slice_mut(&mut osc_guard) };
981 let ma_all = unsafe { mu_slice_as_f64_slice_mut(&mut ma_guard) };
982 let upper_all = unsafe { mu_slice_as_f64_slice_mut(&mut upper_guard) };
983 let lower_all = unsafe { mu_slice_as_f64_slice_mut(&mut lower_guard) };
984 let width_all = unsafe { mu_slice_as_f64_slice_mut(&mut width_guard) };
985 let in_range_all = unsafe { mu_slice_as_f64_slice_mut(&mut in_range_guard) };
986 let trend_all = unsafe { mu_slice_as_f64_slice_mut(&mut trend_guard) };
987 let break_up_all = unsafe { mu_slice_as_f64_slice_mut(&mut break_up_guard) };
988 let break_down_all = unsafe { mu_slice_as_f64_slice_mut(&mut break_down_guard) };
989
990 let run_row = |row: usize,
991 osc_row: &mut [f64],
992 ma_row: &mut [f64],
993 upper_row: &mut [f64],
994 lower_row: &mut [f64],
995 width_row: &mut [f64],
996 in_range_row: &mut [f64],
997 trend_row: &mut [f64],
998 break_up_row: &mut [f64],
999 break_down_row: &mut [f64]|
1000 -> Result<(), RangeOscillatorError> {
1001 let input = RangeOscillatorInput::from_slices(high, low, close, combos[row].clone());
1002 range_oscillator_into_slices(
1003 &input,
1004 single_kernel,
1005 osc_row,
1006 ma_row,
1007 upper_row,
1008 lower_row,
1009 width_row,
1010 in_range_row,
1011 trend_row,
1012 break_up_row,
1013 break_down_row,
1014 )
1015 };
1016
1017 #[cfg(not(target_arch = "wasm32"))]
1018 {
1019 osc_all
1020 .par_chunks_mut(cols)
1021 .zip(ma_all.par_chunks_mut(cols))
1022 .zip(upper_all.par_chunks_mut(cols))
1023 .zip(lower_all.par_chunks_mut(cols))
1024 .zip(width_all.par_chunks_mut(cols))
1025 .zip(in_range_all.par_chunks_mut(cols))
1026 .zip(trend_all.par_chunks_mut(cols))
1027 .zip(break_up_all.par_chunks_mut(cols))
1028 .zip(break_down_all.par_chunks_mut(cols))
1029 .enumerate()
1030 .try_for_each(
1031 |(
1032 row,
1033 (
1034 (
1035 (
1036 (
1037 ((((osc_row, ma_row), upper_row), lower_row), width_row),
1038 in_range_row,
1039 ),
1040 trend_row,
1041 ),
1042 break_up_row,
1043 ),
1044 break_down_row,
1045 ),
1046 )| {
1047 run_row(
1048 row,
1049 osc_row,
1050 ma_row,
1051 upper_row,
1052 lower_row,
1053 width_row,
1054 in_range_row,
1055 trend_row,
1056 break_up_row,
1057 break_down_row,
1058 )
1059 },
1060 )?;
1061 }
1062
1063 #[cfg(target_arch = "wasm32")]
1064 {
1065 for row in 0..rows {
1066 let start = row * cols;
1067 let end = start + cols;
1068 run_row(
1069 row,
1070 &mut osc_all[start..end],
1071 &mut ma_all[start..end],
1072 &mut upper_all[start..end],
1073 &mut lower_all[start..end],
1074 &mut width_all[start..end],
1075 &mut in_range_all[start..end],
1076 &mut trend_all[start..end],
1077 &mut break_up_all[start..end],
1078 &mut break_down_all[start..end],
1079 )?;
1080 }
1081 }
1082
1083 Ok(RangeOscillatorBatchOutput {
1084 oscillator: unsafe { vec_f64_from_mu_guard(osc_guard) },
1085 ma: unsafe { vec_f64_from_mu_guard(ma_guard) },
1086 upper_band: unsafe { vec_f64_from_mu_guard(upper_guard) },
1087 lower_band: unsafe { vec_f64_from_mu_guard(lower_guard) },
1088 range_width: unsafe { vec_f64_from_mu_guard(width_guard) },
1089 in_range: unsafe { vec_f64_from_mu_guard(in_range_guard) },
1090 trend: unsafe { vec_f64_from_mu_guard(trend_guard) },
1091 break_up: unsafe { vec_f64_from_mu_guard(break_up_guard) },
1092 break_down: unsafe { vec_f64_from_mu_guard(break_down_guard) },
1093 combos,
1094 rows,
1095 cols,
1096 })
1097}
1098
1099impl RangeOscillatorStream {
1100 pub fn try_new(params: RangeOscillatorParams) -> Result<Self, RangeOscillatorError> {
1101 let length = params.length.unwrap_or(DEFAULT_LENGTH);
1102 let mult = params.mult.unwrap_or(DEFAULT_MULT);
1103 if length == 0 {
1104 return Err(RangeOscillatorError::InvalidLength {
1105 length,
1106 data_len: 0,
1107 });
1108 }
1109 if !mult.is_finite() || mult < 0.1 {
1110 return Err(RangeOscillatorError::InvalidMult { mult });
1111 }
1112 Ok(Self {
1113 length,
1114 mult,
1115 atr_fallback: AtrState::new(ATR_FALLBACK_PERIOD),
1116 atr_primary: AtrState::new(ATR_PRIMARY_PERIOD),
1117 closes: VecDeque::with_capacity(length + 1),
1118 trend: 0.0,
1119 })
1120 }
1121
1122 #[inline(always)]
1123 pub fn update(
1124 &mut self,
1125 high: f64,
1126 low: f64,
1127 close: f64,
1128 ) -> Option<RangeOscillatorStreamOutput> {
1129 if !high.is_finite() || !low.is_finite() || !close.is_finite() {
1130 self.atr_fallback.reset();
1131 self.atr_primary.reset();
1132 self.closes.clear();
1133 self.trend = 0.0;
1134 return None;
1135 }
1136
1137 let atr200 = self.atr_fallback.update(high, low, close);
1138 let atr2000 = self.atr_primary.update(high, low, close);
1139 if self.closes.len() == self.length + 1 {
1140 self.closes.pop_front();
1141 }
1142 self.closes.push_back(close);
1143
1144 let atr_raw = atr2000.or(atr200)?;
1145 if self.closes.len() < self.length + 1 {
1146 return None;
1147 }
1148 let range_width = atr_raw * self.mult;
1149 compute_point(&self.closes, close, range_width, &mut self.trend)
1150 }
1151}
1152
1153#[inline(always)]
1154unsafe fn mu_slice_as_f64_slice_mut(buf: &mut ManuallyDrop<Vec<MaybeUninit<f64>>>) -> &mut [f64] {
1155 core::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut f64, buf.len())
1156}
1157
1158#[inline(always)]
1159unsafe fn vec_f64_from_mu_guard(buf: ManuallyDrop<Vec<MaybeUninit<f64>>>) -> Vec<f64> {
1160 let mut buf = buf;
1161 Vec::from_raw_parts(buf.as_mut_ptr() as *mut f64, buf.len(), buf.capacity())
1162}
1163
1164#[cfg(feature = "python")]
1165#[pyfunction(name = "range_oscillator")]
1166#[pyo3(signature = (high, low, close, length=DEFAULT_LENGTH, mult=DEFAULT_MULT, kernel=None))]
1167pub fn range_oscillator_py<'py>(
1168 py: Python<'py>,
1169 high: PyReadonlyArray1<'py, f64>,
1170 low: PyReadonlyArray1<'py, f64>,
1171 close: PyReadonlyArray1<'py, f64>,
1172 length: usize,
1173 mult: f64,
1174 kernel: Option<&str>,
1175) -> PyResult<Bound<'py, PyDict>> {
1176 let high = high.as_slice()?;
1177 let low = low.as_slice()?;
1178 let close = close.as_slice()?;
1179 let kernel = validate_kernel(kernel, false)?;
1180 let input = RangeOscillatorInput::from_slices(
1181 high,
1182 low,
1183 close,
1184 RangeOscillatorParams {
1185 length: Some(length),
1186 mult: Some(mult),
1187 },
1188 );
1189 let output = py
1190 .allow_threads(|| range_oscillator_with_kernel(&input, kernel))
1191 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1192 let dict = PyDict::new(py);
1193 dict.set_item("oscillator", output.oscillator.into_pyarray(py))?;
1194 dict.set_item("ma", output.ma.into_pyarray(py))?;
1195 dict.set_item("upper_band", output.upper_band.into_pyarray(py))?;
1196 dict.set_item("lower_band", output.lower_band.into_pyarray(py))?;
1197 dict.set_item("range_width", output.range_width.into_pyarray(py))?;
1198 dict.set_item("in_range", output.in_range.into_pyarray(py))?;
1199 dict.set_item("trend", output.trend.into_pyarray(py))?;
1200 dict.set_item("break_up", output.break_up.into_pyarray(py))?;
1201 dict.set_item("break_down", output.break_down.into_pyarray(py))?;
1202 Ok(dict)
1203}
1204
1205#[cfg(feature = "python")]
1206#[pyfunction(name = "range_oscillator_batch")]
1207#[pyo3(signature = (high, low, close, length_range, mult_range, kernel=None))]
1208pub fn range_oscillator_batch_py<'py>(
1209 py: Python<'py>,
1210 high: PyReadonlyArray1<'py, f64>,
1211 low: PyReadonlyArray1<'py, f64>,
1212 close: PyReadonlyArray1<'py, f64>,
1213 length_range: (usize, usize, usize),
1214 mult_range: (f64, f64, f64),
1215 kernel: Option<&str>,
1216) -> PyResult<Bound<'py, PyDict>> {
1217 let high = high.as_slice()?;
1218 let low = low.as_slice()?;
1219 let close = close.as_slice()?;
1220 let kernel = validate_kernel(kernel, true)?;
1221 let output = py
1222 .allow_threads(|| {
1223 range_oscillator_batch_with_kernel(
1224 high,
1225 low,
1226 close,
1227 &RangeOscillatorBatchRange {
1228 length: length_range,
1229 mult: mult_range,
1230 },
1231 kernel,
1232 )
1233 })
1234 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1235
1236 let total = output.rows * output.cols;
1237 let arrays = [
1238 unsafe { PyArray1::<f64>::new(py, [total], false) },
1239 unsafe { PyArray1::<f64>::new(py, [total], false) },
1240 unsafe { PyArray1::<f64>::new(py, [total], false) },
1241 unsafe { PyArray1::<f64>::new(py, [total], false) },
1242 unsafe { PyArray1::<f64>::new(py, [total], false) },
1243 unsafe { PyArray1::<f64>::new(py, [total], false) },
1244 unsafe { PyArray1::<f64>::new(py, [total], false) },
1245 unsafe { PyArray1::<f64>::new(py, [total], false) },
1246 unsafe { PyArray1::<f64>::new(py, [total], false) },
1247 ];
1248 unsafe { arrays[0].as_slice_mut()? }.copy_from_slice(&output.oscillator);
1249 unsafe { arrays[1].as_slice_mut()? }.copy_from_slice(&output.ma);
1250 unsafe { arrays[2].as_slice_mut()? }.copy_from_slice(&output.upper_band);
1251 unsafe { arrays[3].as_slice_mut()? }.copy_from_slice(&output.lower_band);
1252 unsafe { arrays[4].as_slice_mut()? }.copy_from_slice(&output.range_width);
1253 unsafe { arrays[5].as_slice_mut()? }.copy_from_slice(&output.in_range);
1254 unsafe { arrays[6].as_slice_mut()? }.copy_from_slice(&output.trend);
1255 unsafe { arrays[7].as_slice_mut()? }.copy_from_slice(&output.break_up);
1256 unsafe { arrays[8].as_slice_mut()? }.copy_from_slice(&output.break_down);
1257
1258 let dict = PyDict::new(py);
1259 dict.set_item("oscillator", arrays[0].reshape((output.rows, output.cols))?)?;
1260 dict.set_item("ma", arrays[1].reshape((output.rows, output.cols))?)?;
1261 dict.set_item("upper_band", arrays[2].reshape((output.rows, output.cols))?)?;
1262 dict.set_item("lower_band", arrays[3].reshape((output.rows, output.cols))?)?;
1263 dict.set_item(
1264 "range_width",
1265 arrays[4].reshape((output.rows, output.cols))?,
1266 )?;
1267 dict.set_item("in_range", arrays[5].reshape((output.rows, output.cols))?)?;
1268 dict.set_item("trend", arrays[6].reshape((output.rows, output.cols))?)?;
1269 dict.set_item("break_up", arrays[7].reshape((output.rows, output.cols))?)?;
1270 dict.set_item("break_down", arrays[8].reshape((output.rows, output.cols))?)?;
1271 dict.set_item(
1272 "lengths",
1273 output
1274 .combos
1275 .iter()
1276 .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH) as u64)
1277 .collect::<Vec<_>>()
1278 .into_pyarray(py),
1279 )?;
1280 dict.set_item(
1281 "mults",
1282 output
1283 .combos
1284 .iter()
1285 .map(|combo| combo.mult.unwrap_or(DEFAULT_MULT))
1286 .collect::<Vec<_>>()
1287 .into_pyarray(py),
1288 )?;
1289 dict.set_item("rows", output.rows)?;
1290 dict.set_item("cols", output.cols)?;
1291 Ok(dict)
1292}
1293
1294#[cfg(feature = "python")]
1295#[pyclass(name = "RangeOscillatorStream")]
1296pub struct RangeOscillatorStreamPy {
1297 stream: RangeOscillatorStream,
1298}
1299
1300#[cfg(feature = "python")]
1301#[pymethods]
1302impl RangeOscillatorStreamPy {
1303 #[new]
1304 #[pyo3(signature = (length=DEFAULT_LENGTH, mult=DEFAULT_MULT))]
1305 fn new(length: usize, mult: f64) -> PyResult<Self> {
1306 let stream = RangeOscillatorStream::try_new(RangeOscillatorParams {
1307 length: Some(length),
1308 mult: Some(mult),
1309 })
1310 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1311 Ok(Self { stream })
1312 }
1313
1314 fn update(
1315 &mut self,
1316 high: f64,
1317 low: f64,
1318 close: f64,
1319 ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64, f64)> {
1320 self.stream.update(high, low, close).map(|output| {
1321 (
1322 output.oscillator,
1323 output.ma,
1324 output.upper_band,
1325 output.lower_band,
1326 output.range_width,
1327 output.in_range,
1328 output.trend,
1329 output.break_up,
1330 output.break_down,
1331 )
1332 })
1333 }
1334}
1335
1336#[cfg(feature = "python")]
1337pub fn register_range_oscillator_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
1338 m.add_function(wrap_pyfunction!(range_oscillator_py, m)?)?;
1339 m.add_function(wrap_pyfunction!(range_oscillator_batch_py, m)?)?;
1340 m.add_class::<RangeOscillatorStreamPy>()?;
1341 Ok(())
1342}
1343
1344#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1345#[derive(Serialize, Deserialize)]
1346pub struct RangeOscillatorJsOutput {
1347 pub oscillator: Vec<f64>,
1348 pub ma: Vec<f64>,
1349 pub upper_band: Vec<f64>,
1350 pub lower_band: Vec<f64>,
1351 pub range_width: Vec<f64>,
1352 pub in_range: Vec<f64>,
1353 pub trend: Vec<f64>,
1354 pub break_up: Vec<f64>,
1355 pub break_down: Vec<f64>,
1356}
1357
1358#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1359#[wasm_bindgen(js_name = range_oscillator_js)]
1360pub fn range_oscillator_js(
1361 high: &[f64],
1362 low: &[f64],
1363 close: &[f64],
1364 length: usize,
1365 mult: f64,
1366) -> Result<JsValue, JsValue> {
1367 let input = RangeOscillatorInput::from_slices(
1368 high,
1369 low,
1370 close,
1371 RangeOscillatorParams {
1372 length: Some(length),
1373 mult: Some(mult),
1374 },
1375 );
1376 let output = range_oscillator_with_kernel(&input, Kernel::Auto)
1377 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1378 serde_wasm_bindgen::to_value(&RangeOscillatorJsOutput {
1379 oscillator: output.oscillator,
1380 ma: output.ma,
1381 upper_band: output.upper_band,
1382 lower_band: output.lower_band,
1383 range_width: output.range_width,
1384 in_range: output.in_range,
1385 trend: output.trend,
1386 break_up: output.break_up,
1387 break_down: output.break_down,
1388 })
1389 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1390}
1391
1392#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1393#[derive(Serialize, Deserialize)]
1394pub struct RangeOscillatorBatchConfig {
1395 pub length_range: (usize, usize, usize),
1396 pub mult_range: (f64, f64, f64),
1397}
1398
1399#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1400#[derive(Serialize, Deserialize)]
1401pub struct RangeOscillatorBatchJsOutput {
1402 pub oscillator: Vec<f64>,
1403 pub ma: Vec<f64>,
1404 pub upper_band: Vec<f64>,
1405 pub lower_band: Vec<f64>,
1406 pub range_width: Vec<f64>,
1407 pub in_range: Vec<f64>,
1408 pub trend: Vec<f64>,
1409 pub break_up: Vec<f64>,
1410 pub break_down: Vec<f64>,
1411 pub lengths: Vec<usize>,
1412 pub mults: Vec<f64>,
1413 pub rows: usize,
1414 pub cols: usize,
1415}
1416
1417#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1418#[wasm_bindgen(js_name = range_oscillator_batch)]
1419pub fn range_oscillator_batch_js(
1420 high: &[f64],
1421 low: &[f64],
1422 close: &[f64],
1423 config: JsValue,
1424) -> Result<JsValue, JsValue> {
1425 let cfg: RangeOscillatorBatchConfig =
1426 serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
1427 let output = range_oscillator_batch_with_kernel(
1428 high,
1429 low,
1430 close,
1431 &RangeOscillatorBatchRange {
1432 length: cfg.length_range,
1433 mult: cfg.mult_range,
1434 },
1435 Kernel::Auto,
1436 )
1437 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1438
1439 serde_wasm_bindgen::to_value(&RangeOscillatorBatchJsOutput {
1440 oscillator: output.oscillator,
1441 ma: output.ma,
1442 upper_band: output.upper_band,
1443 lower_band: output.lower_band,
1444 range_width: output.range_width,
1445 in_range: output.in_range,
1446 trend: output.trend,
1447 break_up: output.break_up,
1448 break_down: output.break_down,
1449 lengths: output
1450 .combos
1451 .iter()
1452 .map(|combo| combo.length.unwrap_or(DEFAULT_LENGTH))
1453 .collect(),
1454 mults: output
1455 .combos
1456 .iter()
1457 .map(|combo| combo.mult.unwrap_or(DEFAULT_MULT))
1458 .collect(),
1459 rows: output.rows,
1460 cols: output.cols,
1461 })
1462 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 fn sample_ohlc() -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1470 let mut high = Vec::with_capacity(320);
1471 let mut low = Vec::with_capacity(320);
1472 let mut close = Vec::with_capacity(320);
1473 for i in 0..320 {
1474 let base = 100.0 + i as f64 * 0.18 + (i as f64 * 0.17).sin() * 1.7;
1475 let c = base + (i as f64 * 0.11).cos() * 0.45;
1476 let h = c + 0.9 + (i as f64 * 0.07).sin().abs() * 0.35;
1477 let l = c - 0.9 - (i as f64 * 0.05).cos().abs() * 0.30;
1478 high.push(h);
1479 low.push(l);
1480 close.push(c);
1481 }
1482 (high, low, close)
1483 }
1484
1485 #[test]
1486 fn range_oscillator_into_matches_single() {
1487 let (high, low, close) = sample_ohlc();
1488 let input = RangeOscillatorInput::from_slices(
1489 &high,
1490 &low,
1491 &close,
1492 RangeOscillatorParams {
1493 length: Some(50),
1494 mult: Some(2.0),
1495 },
1496 );
1497 let out = range_oscillator_with_kernel(&input, Kernel::Scalar).expect("single");
1498 let mut oscillator = vec![0.0; close.len()];
1499 let mut ma = vec![0.0; close.len()];
1500 let mut upper = vec![0.0; close.len()];
1501 let mut lower = vec![0.0; close.len()];
1502 let mut width = vec![0.0; close.len()];
1503 let mut in_range = vec![0.0; close.len()];
1504 let mut trend = vec![0.0; close.len()];
1505 let mut break_up = vec![0.0; close.len()];
1506 let mut break_down = vec![0.0; close.len()];
1507
1508 range_oscillator_into_slices(
1509 &input,
1510 Kernel::Scalar,
1511 &mut oscillator,
1512 &mut ma,
1513 &mut upper,
1514 &mut lower,
1515 &mut width,
1516 &mut in_range,
1517 &mut trend,
1518 &mut break_up,
1519 &mut break_down,
1520 )
1521 .expect("into");
1522
1523 for i in 0..close.len() {
1524 for (lhs, rhs) in [
1525 (out.oscillator[i], oscillator[i]),
1526 (out.ma[i], ma[i]),
1527 (out.upper_band[i], upper[i]),
1528 (out.lower_band[i], lower[i]),
1529 (out.range_width[i], width[i]),
1530 (out.in_range[i], in_range[i]),
1531 (out.trend[i], trend[i]),
1532 (out.break_up[i], break_up[i]),
1533 (out.break_down[i], break_down[i]),
1534 ] {
1535 if lhs.is_nan() {
1536 assert!(rhs.is_nan());
1537 } else {
1538 assert!((lhs - rhs).abs() <= 1e-12);
1539 }
1540 }
1541 }
1542 }
1543
1544 #[test]
1545 fn range_oscillator_stream_matches_batch() {
1546 let (high, low, close) = sample_ohlc();
1547 let input = RangeOscillatorInput::from_slices(
1548 &high,
1549 &low,
1550 &close,
1551 RangeOscillatorParams::default(),
1552 );
1553 let out = range_oscillator(&input).expect("batch");
1554 let mut stream =
1555 RangeOscillatorStream::try_new(RangeOscillatorParams::default()).expect("stream");
1556 let mut collected = Vec::with_capacity(close.len());
1557 for i in 0..close.len() {
1558 collected.push(stream.update(high[i], low[i], close[i]));
1559 }
1560 for i in 0..close.len() {
1561 let Some(point) = collected[i] else {
1562 assert!(out.oscillator[i].is_nan());
1563 continue;
1564 };
1565 assert!((point.oscillator - out.oscillator[i]).abs() <= 1e-12);
1566 assert!((point.ma - out.ma[i]).abs() <= 1e-12);
1567 assert!((point.upper_band - out.upper_band[i]).abs() <= 1e-12);
1568 assert!((point.lower_band - out.lower_band[i]).abs() <= 1e-12);
1569 assert!((point.range_width - out.range_width[i]).abs() <= 1e-12);
1570 assert!((point.in_range - out.in_range[i]).abs() <= 1e-12);
1571 assert!((point.trend - out.trend[i]).abs() <= 1e-12);
1572 assert!((point.break_up - out.break_up[i]).abs() <= 1e-12);
1573 assert!((point.break_down - out.break_down[i]).abs() <= 1e-12);
1574 }
1575 }
1576
1577 #[test]
1578 fn range_oscillator_batch_first_row_matches_single() {
1579 let (high, low, close) = sample_ohlc();
1580 let single = range_oscillator(&RangeOscillatorInput::from_slices(
1581 &high,
1582 &low,
1583 &close,
1584 RangeOscillatorParams {
1585 length: Some(50),
1586 mult: Some(2.0),
1587 },
1588 ))
1589 .expect("single");
1590 let batch = range_oscillator_batch_with_kernel(
1591 &high,
1592 &low,
1593 &close,
1594 &RangeOscillatorBatchRange {
1595 length: (50, 52, 2),
1596 mult: (2.0, 2.5, 0.5),
1597 },
1598 Kernel::ScalarBatch,
1599 )
1600 .expect("batch");
1601
1602 assert_eq!(batch.rows, 4);
1603 assert_eq!(batch.cols, close.len());
1604 for i in 0..close.len() {
1605 let idx = i;
1606 for (lhs, rhs) in [
1607 (single.oscillator[i], batch.oscillator[idx]),
1608 (single.ma[i], batch.ma[idx]),
1609 (single.upper_band[i], batch.upper_band[idx]),
1610 (single.lower_band[i], batch.lower_band[idx]),
1611 (single.range_width[i], batch.range_width[idx]),
1612 (single.in_range[i], batch.in_range[idx]),
1613 (single.trend[i], batch.trend[idx]),
1614 (single.break_up[i], batch.break_up[idx]),
1615 (single.break_down[i], batch.break_down[idx]),
1616 ] {
1617 if lhs.is_nan() {
1618 assert!(rhs.is_nan());
1619 } else {
1620 assert!((lhs - rhs).abs() <= 1e-12);
1621 }
1622 }
1623 }
1624 }
1625
1626 #[test]
1627 fn range_oscillator_rejects_invalid_params() {
1628 let (high, low, close) = sample_ohlc();
1629 let err = range_oscillator(&RangeOscillatorInput::from_slices(
1630 &high,
1631 &low,
1632 &close,
1633 RangeOscillatorParams {
1634 length: Some(0),
1635 mult: Some(2.0),
1636 },
1637 ))
1638 .expect_err("invalid length");
1639 assert!(err.to_string().contains("invalid length"));
1640
1641 let err = RangeOscillatorStream::try_new(RangeOscillatorParams {
1642 length: Some(50),
1643 mult: Some(0.0),
1644 })
1645 .expect_err("invalid mult");
1646 assert!(err.to_string().contains("invalid mult"));
1647 }
1648}