Skip to main content

vector_ta/indicators/
range_oscillator.rs

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}