Skip to main content

vector_ta/indicators/
rogers_satchell_volatility.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2pub use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
3
4#[cfg(all(feature = "python", feature = "cuda"))]
5use numpy::PyReadonlyArray2;
6#[cfg(feature = "python")]
7use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
8#[cfg(feature = "python")]
9use pyo3::exceptions::PyValueError;
10#[cfg(feature = "python")]
11use pyo3::prelude::*;
12#[cfg(feature = "python")]
13use pyo3::types::PyDict;
14
15#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
16use serde::{Deserialize, Serialize};
17#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
18use wasm_bindgen::prelude::*;
19
20use crate::utilities::data_loader::Candles;
21use crate::utilities::enums::Kernel;
22use crate::utilities::helpers::{
23    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
24    make_uninit_matrix,
25};
26#[cfg(feature = "python")]
27use crate::utilities::kernel_validation::validate_kernel;
28#[cfg(not(target_arch = "wasm32"))]
29use rayon::prelude::*;
30use std::collections::HashMap;
31use std::mem::{ManuallyDrop, MaybeUninit};
32use thiserror::Error;
33
34#[derive(Debug, Clone)]
35pub enum RogersSatchellVolatilityData<'a> {
36    Candles {
37        candles: &'a Candles,
38    },
39    Slices {
40        open: &'a [f64],
41        high: &'a [f64],
42        low: &'a [f64],
43        close: &'a [f64],
44    },
45}
46
47#[derive(Debug, Clone)]
48pub struct RogersSatchellVolatilityOutput {
49    pub rs: Vec<f64>,
50    pub signal: Vec<f64>,
51}
52
53#[derive(Debug, Clone)]
54#[cfg_attr(
55    all(target_arch = "wasm32", feature = "wasm"),
56    derive(Serialize, Deserialize)
57)]
58pub struct RogersSatchellVolatilityParams {
59    pub lookback: Option<usize>,
60    pub signal_length: Option<usize>,
61}
62
63impl Default for RogersSatchellVolatilityParams {
64    fn default() -> Self {
65        Self {
66            lookback: Some(8),
67            signal_length: Some(8),
68        }
69    }
70}
71
72#[derive(Debug, Clone)]
73pub struct RogersSatchellVolatilityInput<'a> {
74    pub data: RogersSatchellVolatilityData<'a>,
75    pub params: RogersSatchellVolatilityParams,
76}
77
78impl<'a> RogersSatchellVolatilityInput<'a> {
79    #[inline]
80    pub fn from_candles(candles: &'a Candles, params: RogersSatchellVolatilityParams) -> Self {
81        Self {
82            data: RogersSatchellVolatilityData::Candles { candles },
83            params,
84        }
85    }
86
87    #[inline]
88    pub fn from_slices(
89        open: &'a [f64],
90        high: &'a [f64],
91        low: &'a [f64],
92        close: &'a [f64],
93        params: RogersSatchellVolatilityParams,
94    ) -> Self {
95        Self {
96            data: RogersSatchellVolatilityData::Slices {
97                open,
98                high,
99                low,
100                close,
101            },
102            params,
103        }
104    }
105
106    #[inline]
107    pub fn with_default_candles(candles: &'a Candles) -> Self {
108        Self::from_candles(candles, RogersSatchellVolatilityParams::default())
109    }
110
111    #[inline]
112    pub fn get_lookback(&self) -> usize {
113        self.params.lookback.unwrap_or(8)
114    }
115
116    #[inline]
117    pub fn get_signal_length(&self) -> usize {
118        self.params.signal_length.unwrap_or(8)
119    }
120}
121
122#[derive(Copy, Clone, Debug)]
123pub struct RogersSatchellVolatilityBuilder {
124    lookback: Option<usize>,
125    signal_length: Option<usize>,
126    kernel: Kernel,
127}
128
129impl Default for RogersSatchellVolatilityBuilder {
130    fn default() -> Self {
131        Self {
132            lookback: None,
133            signal_length: None,
134            kernel: Kernel::Auto,
135        }
136    }
137}
138
139impl RogersSatchellVolatilityBuilder {
140    #[inline(always)]
141    pub fn new() -> Self {
142        Self::default()
143    }
144
145    #[inline(always)]
146    pub fn lookback(mut self, value: usize) -> Self {
147        self.lookback = Some(value);
148        self
149    }
150
151    #[inline(always)]
152    pub fn signal_length(mut self, value: usize) -> Self {
153        self.signal_length = Some(value);
154        self
155    }
156
157    #[inline(always)]
158    pub fn kernel(mut self, value: Kernel) -> Self {
159        self.kernel = value;
160        self
161    }
162
163    #[inline(always)]
164    pub fn apply(
165        self,
166        candles: &Candles,
167    ) -> Result<RogersSatchellVolatilityOutput, RogersSatchellVolatilityError> {
168        let params = RogersSatchellVolatilityParams {
169            lookback: self.lookback,
170            signal_length: self.signal_length,
171        };
172        let input = RogersSatchellVolatilityInput::from_candles(candles, params);
173        rogers_satchell_volatility_with_kernel(&input, self.kernel)
174    }
175
176    #[inline(always)]
177    pub fn apply_slices(
178        self,
179        open: &[f64],
180        high: &[f64],
181        low: &[f64],
182        close: &[f64],
183    ) -> Result<RogersSatchellVolatilityOutput, RogersSatchellVolatilityError> {
184        let params = RogersSatchellVolatilityParams {
185            lookback: self.lookback,
186            signal_length: self.signal_length,
187        };
188        let input = RogersSatchellVolatilityInput::from_slices(open, high, low, close, params);
189        rogers_satchell_volatility_with_kernel(&input, self.kernel)
190    }
191
192    #[inline(always)]
193    pub fn into_stream(
194        self,
195    ) -> Result<RogersSatchellVolatilityStream, RogersSatchellVolatilityError> {
196        let params = RogersSatchellVolatilityParams {
197            lookback: self.lookback,
198            signal_length: self.signal_length,
199        };
200        RogersSatchellVolatilityStream::try_new(params)
201    }
202}
203
204#[derive(Debug, Error)]
205pub enum RogersSatchellVolatilityError {
206    #[error("rogers_satchell_volatility: Input data slice is empty.")]
207    EmptyInputData,
208    #[error("rogers_satchell_volatility: No valid OHLC values were found.")]
209    NoValidInputData,
210    #[error(
211        "rogers_satchell_volatility: Invalid lookback: lookback = {lookback}, data length = {data_len}"
212    )]
213    InvalidLookback { lookback: usize, data_len: usize },
214    #[error("rogers_satchell_volatility: Invalid signal length: signal_length = {signal_length}")]
215    InvalidSignalLength { signal_length: usize },
216    #[error(
217        "rogers_satchell_volatility: Not enough valid data: needed = {needed}, valid = {valid}"
218    )]
219    NotEnoughValidData { needed: usize, valid: usize },
220    #[error("rogers_satchell_volatility: Inconsistent slice lengths: open={open_len}, high={high_len}, low={low_len}, close={close_len}")]
221    InconsistentSliceLengths {
222        open_len: usize,
223        high_len: usize,
224        low_len: usize,
225        close_len: usize,
226    },
227    #[error(
228        "rogers_satchell_volatility: Output length mismatch: expected = {expected}, got = {got}"
229    )]
230    OutputLengthMismatch { expected: usize, got: usize },
231    #[error("rogers_satchell_volatility: Invalid range: start={start}, end={end}, step={step}")]
232    InvalidRange {
233        start: String,
234        end: String,
235        step: String,
236    },
237    #[error("rogers_satchell_volatility: Invalid kernel for batch: {0:?}")]
238    InvalidKernelForBatch(Kernel),
239}
240
241#[derive(Debug, Clone)]
242pub struct RogersSatchellVolatilityStream {
243    lookback: usize,
244    signal_length: usize,
245    term_ring: Vec<Option<f64>>,
246    term_sum: f64,
247    term_valid: usize,
248    term_idx: usize,
249    term_count: usize,
250    signal_ring: Vec<Option<f64>>,
251    signal_sum: f64,
252    signal_valid: usize,
253    signal_idx: usize,
254    signal_count: usize,
255}
256
257impl RogersSatchellVolatilityStream {
258    #[inline(always)]
259    pub fn try_new(
260        params: RogersSatchellVolatilityParams,
261    ) -> Result<Self, RogersSatchellVolatilityError> {
262        let lookback = params.lookback.unwrap_or(8);
263        if lookback == 0 {
264            return Err(RogersSatchellVolatilityError::InvalidLookback {
265                lookback,
266                data_len: 0,
267            });
268        }
269        let signal_length = params.signal_length.unwrap_or(8);
270        if signal_length == 0 {
271            return Err(RogersSatchellVolatilityError::InvalidSignalLength { signal_length });
272        }
273
274        Ok(Self {
275            lookback,
276            signal_length,
277            term_ring: vec![None; lookback],
278            term_sum: 0.0,
279            term_valid: 0,
280            term_idx: 0,
281            term_count: 0,
282            signal_ring: vec![None; signal_length],
283            signal_sum: 0.0,
284            signal_valid: 0,
285            signal_idx: 0,
286            signal_count: 0,
287        })
288    }
289
290    #[inline(always)]
291    pub fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
292        let term = rs_component(open, high, low, close);
293
294        if self.term_count == self.lookback {
295            if let Some(old) = self.term_ring[self.term_idx] {
296                self.term_sum -= old;
297                self.term_valid -= 1;
298            }
299        } else {
300            self.term_count += 1;
301        }
302        self.term_ring[self.term_idx] = term;
303        if let Some(value) = term {
304            self.term_sum += value;
305            self.term_valid += 1;
306        }
307        self.term_idx += 1;
308        if self.term_idx == self.lookback {
309            self.term_idx = 0;
310        }
311
312        let rs_value = if self.term_count == self.lookback && self.term_valid == self.lookback {
313            let mut variance = self.term_sum / self.lookback as f64;
314            if variance < 0.0 {
315                variance = 0.0;
316            }
317            Some(variance.sqrt())
318        } else {
319            None
320        };
321
322        if self.signal_count == self.signal_length {
323            if let Some(old) = self.signal_ring[self.signal_idx] {
324                self.signal_sum -= old;
325                self.signal_valid -= 1;
326            }
327        } else {
328            self.signal_count += 1;
329        }
330        self.signal_ring[self.signal_idx] = rs_value;
331        if let Some(value) = rs_value {
332            self.signal_sum += value;
333            self.signal_valid += 1;
334        }
335        self.signal_idx += 1;
336        if self.signal_idx == self.signal_length {
337            self.signal_idx = 0;
338        }
339
340        rs_value.map(|rs| {
341            let signal = if self.signal_count == self.signal_length
342                && self.signal_valid == self.signal_length
343            {
344                self.signal_sum / self.signal_length as f64
345            } else {
346                f64::NAN
347            };
348            (rs, signal)
349        })
350    }
351
352    #[inline(always)]
353    pub fn get_warmup_period(&self) -> usize {
354        self.lookback + self.signal_length - 1
355    }
356}
357
358#[inline]
359pub fn rogers_satchell_volatility(
360    input: &RogersSatchellVolatilityInput,
361) -> Result<RogersSatchellVolatilityOutput, RogersSatchellVolatilityError> {
362    rogers_satchell_volatility_with_kernel(input, Kernel::Auto)
363}
364
365#[inline(always)]
366fn validate_ohlc(value: f64) -> bool {
367    value.is_finite() && value > 0.0
368}
369
370#[inline(always)]
371fn rs_component(open: f64, high: f64, low: f64, close: f64) -> Option<f64> {
372    if !validate_ohlc(open) || !validate_ohlc(high) || !validate_ohlc(low) || !validate_ohlc(close)
373    {
374        return None;
375    }
376    Some((high / close).ln() * (high / open).ln() + (low / close).ln() * (low / open).ln())
377}
378
379#[inline(always)]
380fn prepare_input<'a>(
381    input: &'a RogersSatchellVolatilityInput,
382    kernel: Kernel,
383) -> Result<
384    (
385        &'a [f64],
386        &'a [f64],
387        &'a [f64],
388        &'a [f64],
389        usize,
390        usize,
391        Kernel,
392    ),
393    RogersSatchellVolatilityError,
394> {
395    let (open, high, low, close): (&[f64], &[f64], &[f64], &[f64]) = match &input.data {
396        RogersSatchellVolatilityData::Candles { candles } => {
397            (&candles.open, &candles.high, &candles.low, &candles.close)
398        }
399        RogersSatchellVolatilityData::Slices {
400            open,
401            high,
402            low,
403            close,
404        } => (open, high, low, close),
405    };
406
407    let len = close.len();
408    if len == 0 {
409        return Err(RogersSatchellVolatilityError::EmptyInputData);
410    }
411    if open.len() != len || high.len() != len || low.len() != len {
412        return Err(RogersSatchellVolatilityError::InconsistentSliceLengths {
413            open_len: open.len(),
414            high_len: high.len(),
415            low_len: low.len(),
416            close_len: close.len(),
417        });
418    }
419
420    let lookback = input.get_lookback();
421    if lookback == 0 || lookback > len {
422        return Err(RogersSatchellVolatilityError::InvalidLookback {
423            lookback,
424            data_len: len,
425        });
426    }
427
428    let signal_length = input.get_signal_length();
429    if signal_length == 0 {
430        return Err(RogersSatchellVolatilityError::InvalidSignalLength { signal_length });
431    }
432
433    let valid = open
434        .iter()
435        .zip(high.iter())
436        .zip(low.iter())
437        .zip(close.iter())
438        .filter(|(((o, h), l), c)| {
439            validate_ohlc(**o) && validate_ohlc(**h) && validate_ohlc(**l) && validate_ohlc(**c)
440        })
441        .count();
442    if valid == 0 {
443        return Err(RogersSatchellVolatilityError::NoValidInputData);
444    }
445    if valid < lookback {
446        return Err(RogersSatchellVolatilityError::NotEnoughValidData {
447            needed: lookback,
448            valid,
449        });
450    }
451
452    let chosen = match kernel {
453        Kernel::Auto => detect_best_kernel(),
454        value => value.to_non_batch(),
455    };
456
457    Ok((open, high, low, close, lookback, signal_length, chosen))
458}
459
460#[inline(always)]
461fn build_term_prefixes(
462    open: &[f64],
463    high: &[f64],
464    low: &[f64],
465    close: &[f64],
466) -> (Vec<usize>, Vec<f64>) {
467    let len = close.len();
468    let mut prefix_valid = vec![0usize; len + 1];
469    let mut prefix_sum = vec![0.0f64; len + 1];
470
471    for i in 0..len {
472        prefix_valid[i + 1] = prefix_valid[i];
473        prefix_sum[i + 1] = prefix_sum[i];
474        if let Some(term) = rs_component(open[i], high[i], low[i], close[i]) {
475            prefix_valid[i + 1] += 1;
476            prefix_sum[i + 1] += term;
477        }
478    }
479
480    (prefix_valid, prefix_sum)
481}
482
483#[inline(always)]
484fn compute_rs_from_prefix(
485    prefix_valid: &[usize],
486    prefix_sum: &[f64],
487    lookback: usize,
488    out: &mut [f64],
489) {
490    let len = out.len();
491    if len == 0 {
492        return;
493    }
494    let warm = lookback.saturating_sub(1).min(len);
495    for value in &mut out[..warm] {
496        *value = f64::NAN;
497    }
498    for t in warm..len {
499        let end = t + 1;
500        let start = end - lookback;
501        if prefix_valid[end] - prefix_valid[start] == lookback {
502            let mut variance = (prefix_sum[end] - prefix_sum[start]) / lookback as f64;
503            if variance < 0.0 {
504                variance = 0.0;
505            }
506            out[t] = variance.sqrt();
507        } else {
508            out[t] = f64::NAN;
509        }
510    }
511}
512
513#[inline(always)]
514fn compute_signal_from_rs(rs: &[f64], signal_length: usize, out: &mut [f64]) {
515    let len = rs.len();
516    if len == 0 {
517        return;
518    }
519    let mut prefix_valid = vec![0usize; len + 1];
520    let mut prefix_sum = vec![0.0f64; len + 1];
521    for i in 0..len {
522        prefix_valid[i + 1] = prefix_valid[i];
523        prefix_sum[i + 1] = prefix_sum[i];
524        let value = rs[i];
525        if value.is_finite() {
526            prefix_valid[i + 1] += 1;
527            prefix_sum[i + 1] += value;
528        }
529    }
530
531    let warm = signal_length.saturating_sub(1).min(len);
532    for value in &mut out[..warm] {
533        *value = f64::NAN;
534    }
535    for t in warm..len {
536        let end = t + 1;
537        let start = end - signal_length;
538        if prefix_valid[end] - prefix_valid[start] == signal_length {
539            out[t] = (prefix_sum[end] - prefix_sum[start]) / signal_length as f64;
540        } else {
541            out[t] = f64::NAN;
542        }
543    }
544}
545
546#[inline(always)]
547fn rogers_satchell_volatility_compute_into(
548    open: &[f64],
549    high: &[f64],
550    low: &[f64],
551    close: &[f64],
552    lookback: usize,
553    signal_length: usize,
554    _kernel: Kernel,
555    out_rs: &mut [f64],
556    out_signal: &mut [f64],
557) {
558    let (prefix_valid, prefix_sum) = build_term_prefixes(open, high, low, close);
559    compute_rs_from_prefix(&prefix_valid, &prefix_sum, lookback, out_rs);
560    compute_signal_from_rs(out_rs, signal_length, out_signal);
561}
562
563#[inline]
564pub fn rogers_satchell_volatility_with_kernel(
565    input: &RogersSatchellVolatilityInput,
566    kernel: Kernel,
567) -> Result<RogersSatchellVolatilityOutput, RogersSatchellVolatilityError> {
568    let (open, high, low, close, lookback, signal_length, chosen) = prepare_input(input, kernel)?;
569    let len = close.len();
570
571    let mut rs = alloc_with_nan_prefix(len, lookback.saturating_sub(1));
572    let signal_warm = lookback
573        .saturating_sub(1)
574        .saturating_add(signal_length.saturating_sub(1));
575    let mut signal = alloc_with_nan_prefix(len, signal_warm);
576
577    rogers_satchell_volatility_compute_into(
578        open,
579        high,
580        low,
581        close,
582        lookback,
583        signal_length,
584        chosen,
585        &mut rs,
586        &mut signal,
587    );
588
589    Ok(RogersSatchellVolatilityOutput { rs, signal })
590}
591
592#[inline]
593pub fn rogers_satchell_volatility_into_slice(
594    out_rs: &mut [f64],
595    out_signal: &mut [f64],
596    input: &RogersSatchellVolatilityInput,
597    kernel: Kernel,
598) -> Result<(), RogersSatchellVolatilityError> {
599    let (open, high, low, close, lookback, signal_length, chosen) = prepare_input(input, kernel)?;
600    let expected = close.len();
601    if out_rs.len() != expected || out_signal.len() != expected {
602        return Err(RogersSatchellVolatilityError::OutputLengthMismatch {
603            expected,
604            got: out_rs.len().max(out_signal.len()),
605        });
606    }
607
608    rogers_satchell_volatility_compute_into(
609        open,
610        high,
611        low,
612        close,
613        lookback,
614        signal_length,
615        chosen,
616        out_rs,
617        out_signal,
618    );
619    Ok(())
620}
621
622#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
623#[inline]
624pub fn rogers_satchell_volatility_into(
625    input: &RogersSatchellVolatilityInput,
626    out_rs: &mut [f64],
627    out_signal: &mut [f64],
628) -> Result<(), RogersSatchellVolatilityError> {
629    rogers_satchell_volatility_into_slice(out_rs, out_signal, input, Kernel::Auto)
630}
631
632#[derive(Debug, Clone)]
633#[cfg_attr(
634    all(target_arch = "wasm32", feature = "wasm"),
635    derive(Serialize, Deserialize)
636)]
637pub struct RogersSatchellVolatilityBatchRange {
638    pub lookback: (usize, usize, usize),
639    pub signal_length: (usize, usize, usize),
640}
641
642impl Default for RogersSatchellVolatilityBatchRange {
643    fn default() -> Self {
644        Self {
645            lookback: (8, 252, 1),
646            signal_length: (8, 8, 0),
647        }
648    }
649}
650
651#[derive(Clone, Debug, Default)]
652pub struct RogersSatchellVolatilityBatchBuilder {
653    range: RogersSatchellVolatilityBatchRange,
654    kernel: Kernel,
655}
656
657impl RogersSatchellVolatilityBatchBuilder {
658    pub fn new() -> Self {
659        Self::default()
660    }
661
662    pub fn kernel(mut self, value: Kernel) -> Self {
663        self.kernel = value;
664        self
665    }
666
667    #[inline]
668    pub fn lookback_range(mut self, start: usize, end: usize, step: usize) -> Self {
669        self.range.lookback = (start, end, step);
670        self
671    }
672
673    #[inline]
674    pub fn lookback_static(mut self, value: usize) -> Self {
675        self.range.lookback = (value, value, 0);
676        self
677    }
678
679    #[inline]
680    pub fn signal_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
681        self.range.signal_length = (start, end, step);
682        self
683    }
684
685    #[inline]
686    pub fn signal_length_static(mut self, value: usize) -> Self {
687        self.range.signal_length = (value, value, 0);
688        self
689    }
690
691    pub fn apply_slices(
692        self,
693        open: &[f64],
694        high: &[f64],
695        low: &[f64],
696        close: &[f64],
697    ) -> Result<RogersSatchellVolatilityBatchOutput, RogersSatchellVolatilityError> {
698        rogers_satchell_volatility_batch_with_kernel(
699            open,
700            high,
701            low,
702            close,
703            &self.range,
704            self.kernel,
705        )
706    }
707
708    pub fn apply_candles(
709        self,
710        candles: &Candles,
711    ) -> Result<RogersSatchellVolatilityBatchOutput, RogersSatchellVolatilityError> {
712        self.apply_slices(&candles.open, &candles.high, &candles.low, &candles.close)
713    }
714}
715
716#[derive(Clone, Debug)]
717pub struct RogersSatchellVolatilityBatchOutput {
718    pub rs: Vec<f64>,
719    pub signal: Vec<f64>,
720    pub combos: Vec<RogersSatchellVolatilityParams>,
721    pub rows: usize,
722    pub cols: usize,
723}
724
725impl RogersSatchellVolatilityBatchOutput {
726    pub fn row_for_params(&self, params: &RogersSatchellVolatilityParams) -> Option<usize> {
727        let lookback = params.lookback.unwrap_or(8);
728        let signal_length = params.signal_length.unwrap_or(8);
729        self.combos.iter().position(|combo| {
730            combo.lookback.unwrap_or(8) == lookback
731                && combo.signal_length.unwrap_or(8) == signal_length
732        })
733    }
734
735    pub fn rs_for(&self, params: &RogersSatchellVolatilityParams) -> Option<&[f64]> {
736        self.row_for_params(params).and_then(|row| {
737            row.checked_mul(self.cols)
738                .and_then(|start| self.rs.get(start..start + self.cols))
739        })
740    }
741
742    pub fn signal_for(&self, params: &RogersSatchellVolatilityParams) -> Option<&[f64]> {
743        self.row_for_params(params).and_then(|row| {
744            row.checked_mul(self.cols)
745                .and_then(|start| self.signal.get(start..start + self.cols))
746        })
747    }
748}
749
750#[inline(always)]
751fn axis_usize(
752    (start, end, step): (usize, usize, usize),
753) -> Result<Vec<usize>, RogersSatchellVolatilityError> {
754    if step == 0 || start == end {
755        return Ok(vec![start]);
756    }
757    let step = step.max(1);
758    if start < end {
759        let mut values = Vec::new();
760        let mut current = start;
761        while current <= end {
762            values.push(current);
763            match current.checked_add(step) {
764                Some(next) if next != current => current = next,
765                _ => break,
766            }
767        }
768        if values.is_empty() {
769            return Err(RogersSatchellVolatilityError::InvalidRange {
770                start: start.to_string(),
771                end: end.to_string(),
772                step: step.to_string(),
773            });
774        }
775        Ok(values)
776    } else {
777        let mut values = Vec::new();
778        let mut current = start;
779        loop {
780            values.push(current);
781            if current == end {
782                break;
783            }
784            let next = current.saturating_sub(step);
785            if next == current || next < end {
786                break;
787            }
788            current = next;
789        }
790        if values.is_empty() {
791            return Err(RogersSatchellVolatilityError::InvalidRange {
792                start: start.to_string(),
793                end: end.to_string(),
794                step: step.to_string(),
795            });
796        }
797        Ok(values)
798    }
799}
800
801#[inline(always)]
802fn expand_grid_rogers_satchell(
803    range: &RogersSatchellVolatilityBatchRange,
804) -> Result<Vec<RogersSatchellVolatilityParams>, RogersSatchellVolatilityError> {
805    let lookbacks = axis_usize(range.lookback)?;
806    let signal_lengths = axis_usize(range.signal_length)?;
807    let total = lookbacks
808        .len()
809        .checked_mul(signal_lengths.len())
810        .ok_or_else(|| RogersSatchellVolatilityError::InvalidRange {
811            start: "lookback".to_string(),
812            end: "signal_length".to_string(),
813            step: "overflow".to_string(),
814        })?;
815    let mut combos = Vec::with_capacity(total);
816    for &lookback in &lookbacks {
817        for &signal_length in &signal_lengths {
818            combos.push(RogersSatchellVolatilityParams {
819                lookback: Some(lookback),
820                signal_length: Some(signal_length),
821            });
822        }
823    }
824    Ok(combos)
825}
826
827#[inline(always)]
828fn fill_row_from_cache(
829    combo: &RogersSatchellVolatilityParams,
830    raw_cache: &HashMap<usize, Vec<f64>>,
831    signal_cache: &HashMap<(usize, usize), Vec<f64>>,
832    rs_row: &mut [f64],
833    signal_row: &mut [f64],
834) {
835    let lookback = combo.lookback.unwrap_or(8);
836    let signal_length = combo.signal_length.unwrap_or(8);
837    rs_row.copy_from_slice(raw_cache.get(&lookback).unwrap());
838    signal_row.copy_from_slice(signal_cache.get(&(lookback, signal_length)).unwrap());
839}
840
841#[inline(always)]
842fn rogers_satchell_volatility_batch_inner_into(
843    open: &[f64],
844    high: &[f64],
845    low: &[f64],
846    close: &[f64],
847    sweep: &RogersSatchellVolatilityBatchRange,
848    kernel: Kernel,
849    parallel: bool,
850    out_rs: &mut [f64],
851    out_signal: &mut [f64],
852) -> Result<Vec<RogersSatchellVolatilityParams>, RogersSatchellVolatilityError> {
853    let len = close.len();
854    if len == 0 {
855        return Err(RogersSatchellVolatilityError::EmptyInputData);
856    }
857    if open.len() != len || high.len() != len || low.len() != len {
858        return Err(RogersSatchellVolatilityError::InconsistentSliceLengths {
859            open_len: open.len(),
860            high_len: high.len(),
861            low_len: low.len(),
862            close_len: close.len(),
863        });
864    }
865
866    let combos = expand_grid_rogers_satchell(sweep)?;
867    let rows = combos.len();
868    let expected = rows.checked_mul(len).ok_or_else(|| {
869        RogersSatchellVolatilityError::OutputLengthMismatch {
870            expected: usize::MAX,
871            got: out_rs.len().max(out_signal.len()),
872        }
873    })?;
874    if out_rs.len() != expected || out_signal.len() != expected {
875        return Err(RogersSatchellVolatilityError::OutputLengthMismatch {
876            expected,
877            got: out_rs.len().max(out_signal.len()),
878        });
879    }
880
881    let kernel = match kernel {
882        Kernel::Auto => detect_best_batch_kernel().to_non_batch(),
883        value => value.to_non_batch(),
884    };
885
886    let max_lookback = combos
887        .iter()
888        .map(|combo| combo.lookback.unwrap_or(8))
889        .max()
890        .unwrap_or(8);
891    let valid = open
892        .iter()
893        .zip(high.iter())
894        .zip(low.iter())
895        .zip(close.iter())
896        .filter(|(((o, h), l), c)| {
897            validate_ohlc(**o) && validate_ohlc(**h) && validate_ohlc(**l) && validate_ohlc(**c)
898        })
899        .count();
900    if valid == 0 {
901        return Err(RogersSatchellVolatilityError::NoValidInputData);
902    }
903    if valid < max_lookback {
904        return Err(RogersSatchellVolatilityError::NotEnoughValidData {
905            needed: max_lookback,
906            valid,
907        });
908    }
909
910    for combo in &combos {
911        let lookback = combo.lookback.unwrap_or(8);
912        if lookback == 0 || lookback > len {
913            return Err(RogersSatchellVolatilityError::InvalidLookback {
914                lookback,
915                data_len: len,
916            });
917        }
918        let signal_length = combo.signal_length.unwrap_or(8);
919        if signal_length == 0 {
920            return Err(RogersSatchellVolatilityError::InvalidSignalLength { signal_length });
921        }
922    }
923
924    let (prefix_valid, prefix_sum) = build_term_prefixes(open, high, low, close);
925    let mut raw_cache: HashMap<usize, Vec<f64>> = HashMap::new();
926    let mut signal_cache: HashMap<(usize, usize), Vec<f64>> = HashMap::new();
927
928    for combo in &combos {
929        let lookback = combo.lookback.unwrap_or(8);
930        raw_cache.entry(lookback).or_insert_with(|| {
931            let mut row = alloc_with_nan_prefix(len, lookback.saturating_sub(1));
932            compute_rs_from_prefix(&prefix_valid, &prefix_sum, lookback, &mut row);
933            row
934        });
935        let signal_length = combo.signal_length.unwrap_or(8);
936        signal_cache
937            .entry((lookback, signal_length))
938            .or_insert_with(|| {
939                let raw = raw_cache.get(&lookback).unwrap();
940                let warm = lookback
941                    .saturating_sub(1)
942                    .saturating_add(signal_length.saturating_sub(1));
943                let mut row = alloc_with_nan_prefix(len, warm);
944                compute_signal_from_rs(raw, signal_length, &mut row);
945                row
946            });
947    }
948
949    #[cfg(not(target_arch = "wasm32"))]
950    if parallel {
951        out_rs
952            .par_chunks_mut(len)
953            .zip(out_signal.par_chunks_mut(len))
954            .zip(combos.par_iter())
955            .for_each(|((rs_row, signal_row), combo)| {
956                fill_row_from_cache(combo, &raw_cache, &signal_cache, rs_row, signal_row);
957            });
958        let _ = kernel;
959        return Ok(combos);
960    }
961
962    for ((rs_row, signal_row), combo) in out_rs
963        .chunks_mut(len)
964        .zip(out_signal.chunks_mut(len))
965        .zip(combos.iter())
966    {
967        fill_row_from_cache(combo, &raw_cache, &signal_cache, rs_row, signal_row);
968    }
969    let _ = kernel;
970    Ok(combos)
971}
972
973#[inline]
974pub fn rogers_satchell_volatility_batch_with_kernel(
975    open: &[f64],
976    high: &[f64],
977    low: &[f64],
978    close: &[f64],
979    sweep: &RogersSatchellVolatilityBatchRange,
980    kernel: Kernel,
981) -> Result<RogersSatchellVolatilityBatchOutput, RogersSatchellVolatilityError> {
982    let combos = expand_grid_rogers_satchell(sweep)?;
983    if combos.is_empty() {
984        return Err(RogersSatchellVolatilityError::InvalidRange {
985            start: "range".to_string(),
986            end: "range".to_string(),
987            step: "empty".to_string(),
988        });
989    }
990    let rows = combos.len();
991    let cols = close.len();
992
993    let rs_warm: Vec<usize> = combos
994        .iter()
995        .map(|combo| combo.lookback.unwrap_or(8).saturating_sub(1).min(cols))
996        .collect();
997    let signal_warm: Vec<usize> = combos
998        .iter()
999        .map(|combo| {
1000            combo
1001                .lookback
1002                .unwrap_or(8)
1003                .saturating_sub(1)
1004                .saturating_add(combo.signal_length.unwrap_or(8).saturating_sub(1))
1005                .min(cols)
1006        })
1007        .collect();
1008
1009    let mut rs_mu = make_uninit_matrix(rows, cols);
1010    let mut signal_mu = make_uninit_matrix(rows, cols);
1011    init_matrix_prefixes(&mut rs_mu, cols, &rs_warm);
1012    init_matrix_prefixes(&mut signal_mu, cols, &signal_warm);
1013
1014    let mut rs_guard = ManuallyDrop::new(rs_mu);
1015    let mut signal_guard = ManuallyDrop::new(signal_mu);
1016    let rs_out: &mut [f64] = unsafe {
1017        core::slice::from_raw_parts_mut(rs_guard.as_mut_ptr() as *mut f64, rs_guard.len())
1018    };
1019    let signal_out: &mut [f64] = unsafe {
1020        core::slice::from_raw_parts_mut(signal_guard.as_mut_ptr() as *mut f64, signal_guard.len())
1021    };
1022
1023    let resolved = rogers_satchell_volatility_batch_inner_into(
1024        open, high, low, close, sweep, kernel, true, rs_out, signal_out,
1025    )?;
1026
1027    let rs = unsafe {
1028        Vec::from_raw_parts(
1029            rs_guard.as_mut_ptr() as *mut f64,
1030            rs_guard.len(),
1031            rs_guard.capacity(),
1032        )
1033    };
1034    let signal = unsafe {
1035        Vec::from_raw_parts(
1036            signal_guard.as_mut_ptr() as *mut f64,
1037            signal_guard.len(),
1038            signal_guard.capacity(),
1039        )
1040    };
1041
1042    Ok(RogersSatchellVolatilityBatchOutput {
1043        rs,
1044        signal,
1045        combos: resolved,
1046        rows,
1047        cols,
1048    })
1049}
1050
1051#[inline]
1052pub fn rogers_satchell_volatility_batch_slice(
1053    open: &[f64],
1054    high: &[f64],
1055    low: &[f64],
1056    close: &[f64],
1057    sweep: &RogersSatchellVolatilityBatchRange,
1058    kernel: Kernel,
1059) -> Result<RogersSatchellVolatilityBatchOutput, RogersSatchellVolatilityError> {
1060    rogers_satchell_volatility_batch_with_kernel(open, high, low, close, sweep, kernel)
1061}
1062
1063#[inline]
1064pub fn rogers_satchell_volatility_batch_par_slice(
1065    open: &[f64],
1066    high: &[f64],
1067    low: &[f64],
1068    close: &[f64],
1069    sweep: &RogersSatchellVolatilityBatchRange,
1070    kernel: Kernel,
1071) -> Result<RogersSatchellVolatilityBatchOutput, RogersSatchellVolatilityError> {
1072    rogers_satchell_volatility_batch_with_kernel(open, high, low, close, sweep, kernel)
1073}
1074
1075#[cfg(feature = "python")]
1076#[pyfunction(name = "rogers_satchell_volatility")]
1077#[pyo3(signature = (open, high, low, close, lookback=8, signal_length=8, kernel=None))]
1078pub fn rogers_satchell_volatility_py<'py>(
1079    py: Python<'py>,
1080    open: PyReadonlyArray1<'py, f64>,
1081    high: PyReadonlyArray1<'py, f64>,
1082    low: PyReadonlyArray1<'py, f64>,
1083    close: PyReadonlyArray1<'py, f64>,
1084    lookback: usize,
1085    signal_length: usize,
1086    kernel: Option<&str>,
1087) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1088    let open = open.as_slice()?;
1089    let high = high.as_slice()?;
1090    let low = low.as_slice()?;
1091    let close = close.as_slice()?;
1092    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1093        return Err(PyValueError::new_err("OHLC slice length mismatch"));
1094    }
1095
1096    let kernel = validate_kernel(kernel, false)?;
1097    let input = RogersSatchellVolatilityInput::from_slices(
1098        open,
1099        high,
1100        low,
1101        close,
1102        RogersSatchellVolatilityParams {
1103            lookback: Some(lookback),
1104            signal_length: Some(signal_length),
1105        },
1106    );
1107    let out = py
1108        .allow_threads(|| rogers_satchell_volatility_with_kernel(&input, kernel))
1109        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1110    Ok((out.rs.into_pyarray(py), out.signal.into_pyarray(py)))
1111}
1112
1113#[cfg(feature = "python")]
1114#[pyclass(name = "RogersSatchellVolatilityStream")]
1115pub struct RogersSatchellVolatilityStreamPy {
1116    stream: RogersSatchellVolatilityStream,
1117}
1118
1119#[cfg(feature = "python")]
1120#[pymethods]
1121impl RogersSatchellVolatilityStreamPy {
1122    #[new]
1123    fn new(lookback: usize, signal_length: usize) -> PyResult<Self> {
1124        let stream = RogersSatchellVolatilityStream::try_new(RogersSatchellVolatilityParams {
1125            lookback: Some(lookback),
1126            signal_length: Some(signal_length),
1127        })
1128        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1129        Ok(Self { stream })
1130    }
1131
1132    fn update(&mut self, open: f64, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
1133        self.stream.update(open, high, low, close)
1134    }
1135}
1136
1137#[cfg(feature = "python")]
1138#[pyfunction(name = "rogers_satchell_volatility_batch")]
1139#[pyo3(signature = (open, high, low, close, lookback_range=(8,8,0), signal_length_range=(8,8,0), kernel=None))]
1140pub fn rogers_satchell_volatility_batch_py<'py>(
1141    py: Python<'py>,
1142    open: PyReadonlyArray1<'py, f64>,
1143    high: PyReadonlyArray1<'py, f64>,
1144    low: PyReadonlyArray1<'py, f64>,
1145    close: PyReadonlyArray1<'py, f64>,
1146    lookback_range: (usize, usize, usize),
1147    signal_length_range: (usize, usize, usize),
1148    kernel: Option<&str>,
1149) -> PyResult<Bound<'py, PyDict>> {
1150    let open = open.as_slice()?;
1151    let high = high.as_slice()?;
1152    let low = low.as_slice()?;
1153    let close = close.as_slice()?;
1154    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1155        return Err(PyValueError::new_err("OHLC slice length mismatch"));
1156    }
1157
1158    let sweep = RogersSatchellVolatilityBatchRange {
1159        lookback: lookback_range,
1160        signal_length: signal_length_range,
1161    };
1162    let combos =
1163        expand_grid_rogers_satchell(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1164    let rows = combos.len();
1165    let cols = close.len();
1166    let total = rows
1167        .checked_mul(cols)
1168        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1169
1170    let rs_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1171    let signal_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1172    let rs_out = unsafe { rs_arr.as_slice_mut()? };
1173    let signal_out = unsafe { signal_arr.as_slice_mut()? };
1174
1175    let kernel = validate_kernel(kernel, true)?;
1176    py.allow_threads(|| {
1177        rogers_satchell_volatility_batch_inner_into(
1178            open, high, low, close, &sweep, kernel, true, rs_out, signal_out,
1179        )
1180    })
1181    .map_err(|e| PyValueError::new_err(e.to_string()))?;
1182
1183    let dict = PyDict::new(py);
1184    dict.set_item("rs", rs_arr.reshape((rows, cols))?)?;
1185    dict.set_item("signal", signal_arr.reshape((rows, cols))?)?;
1186    dict.set_item(
1187        "lookbacks",
1188        combos
1189            .iter()
1190            .map(|combo| combo.lookback.unwrap_or(8) as u64)
1191            .collect::<Vec<_>>()
1192            .into_pyarray(py),
1193    )?;
1194    dict.set_item(
1195        "signal_lengths",
1196        combos
1197            .iter()
1198            .map(|combo| combo.signal_length.unwrap_or(8) as u64)
1199            .collect::<Vec<_>>()
1200            .into_pyarray(py),
1201    )?;
1202    dict.set_item("rows", rows)?;
1203    dict.set_item("cols", cols)?;
1204    Ok(dict)
1205}
1206
1207#[cfg(all(feature = "python", feature = "cuda"))]
1208#[pyfunction(name = "rogers_satchell_volatility_cuda_batch_dev")]
1209#[pyo3(signature = (open_f32, high_f32, low_f32, close_f32, lookback_range=(8,8,0), signal_length_range=(8,8,0), device_id=0))]
1210pub fn rogers_satchell_volatility_cuda_batch_dev_py<'py>(
1211    py: Python<'py>,
1212    open_f32: PyReadonlyArray1<'py, f32>,
1213    high_f32: PyReadonlyArray1<'py, f32>,
1214    low_f32: PyReadonlyArray1<'py, f32>,
1215    close_f32: PyReadonlyArray1<'py, f32>,
1216    lookback_range: (usize, usize, usize),
1217    signal_length_range: (usize, usize, usize),
1218    device_id: usize,
1219) -> PyResult<Bound<'py, PyDict>> {
1220    use crate::cuda::cuda_available;
1221    use crate::cuda::CudaRogersSatchellVolatility;
1222
1223    if !cuda_available() {
1224        return Err(PyValueError::new_err("CUDA not available"));
1225    }
1226
1227    let open = open_f32.as_slice()?;
1228    let high = high_f32.as_slice()?;
1229    let low = low_f32.as_slice()?;
1230    let close = close_f32.as_slice()?;
1231    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1232        return Err(PyValueError::new_err("OHLC slice length mismatch"));
1233    }
1234
1235    let sweep = RogersSatchellVolatilityBatchRange {
1236        lookback: lookback_range,
1237        signal_length: signal_length_range,
1238    };
1239    let combos =
1240        expand_grid_rogers_satchell(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1241
1242    let dict = PyDict::new(py);
1243    let (result, ctx, dev_id) = py.allow_threads(|| {
1244        let cuda = CudaRogersSatchellVolatility::new(device_id)
1245            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1246        let ctx = cuda.context_arc_clone();
1247        let dev_id = cuda.device_id();
1248        cuda.rogers_satchell_volatility_batch_dev(open, high, low, close, &sweep)
1249            .map(|result| (result, ctx, dev_id))
1250            .map_err(|e| PyValueError::new_err(e.to_string()))
1251    })?;
1252    let rows = result.outputs.rs.rows;
1253    let cols = result.outputs.rs.cols;
1254
1255    dict.set_item(
1256        "rs",
1257        DeviceArrayF32Py {
1258            inner: result.outputs.rs,
1259            _ctx: Some(ctx.clone()),
1260            device_id: Some(dev_id),
1261        },
1262    )?;
1263    dict.set_item(
1264        "signal",
1265        DeviceArrayF32Py {
1266            inner: result.outputs.signal,
1267            _ctx: Some(ctx),
1268            device_id: Some(dev_id),
1269        },
1270    )?;
1271    dict.set_item(
1272        "lookbacks",
1273        combos
1274            .iter()
1275            .map(|combo| combo.lookback.unwrap_or(8) as u64)
1276            .collect::<Vec<_>>()
1277            .into_pyarray(py),
1278    )?;
1279    dict.set_item(
1280        "signal_lengths",
1281        combos
1282            .iter()
1283            .map(|combo| combo.signal_length.unwrap_or(8) as u64)
1284            .collect::<Vec<_>>()
1285            .into_pyarray(py),
1286    )?;
1287    dict.set_item("rows", rows)?;
1288    dict.set_item("cols", cols)?;
1289    Ok(dict)
1290}
1291
1292#[cfg(all(feature = "python", feature = "cuda"))]
1293#[pyfunction(name = "rogers_satchell_volatility_cuda_many_series_one_param_dev")]
1294#[pyo3(signature = (open_tm_f32, high_tm_f32, low_tm_f32, close_tm_f32, lookback=8, signal_length=8, device_id=0))]
1295pub fn rogers_satchell_volatility_cuda_many_series_one_param_dev_py<'py>(
1296    py: Python<'py>,
1297    open_tm_f32: PyReadonlyArray2<'py, f32>,
1298    high_tm_f32: PyReadonlyArray2<'py, f32>,
1299    low_tm_f32: PyReadonlyArray2<'py, f32>,
1300    close_tm_f32: PyReadonlyArray2<'py, f32>,
1301    lookback: usize,
1302    signal_length: usize,
1303    device_id: usize,
1304) -> PyResult<Bound<'py, PyDict>> {
1305    use crate::cuda::cuda_available;
1306    use crate::cuda::CudaRogersSatchellVolatility;
1307    use numpy::PyUntypedArrayMethods;
1308
1309    if !cuda_available() {
1310        return Err(PyValueError::new_err("CUDA not available"));
1311    }
1312
1313    let rows = close_tm_f32.shape()[0];
1314    let cols = close_tm_f32.shape()[1];
1315    if open_tm_f32.shape() != high_tm_f32.shape()
1316        || open_tm_f32.shape() != low_tm_f32.shape()
1317        || open_tm_f32.shape() != close_tm_f32.shape()
1318    {
1319        return Err(PyValueError::new_err("OHLC matrix shape mismatch"));
1320    }
1321
1322    let open = open_tm_f32.as_slice()?;
1323    let high = high_tm_f32.as_slice()?;
1324    let low = low_tm_f32.as_slice()?;
1325    let close = close_tm_f32.as_slice()?;
1326
1327    let dict = PyDict::new(py);
1328    let (result, ctx, dev_id) = py.allow_threads(|| {
1329        let cuda = CudaRogersSatchellVolatility::new(device_id)
1330            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1331        let ctx = cuda.context_arc_clone();
1332        let dev_id = cuda.device_id();
1333        cuda.rogers_satchell_volatility_many_series_one_param_time_major_dev(
1334            open,
1335            high,
1336            low,
1337            close,
1338            cols,
1339            rows,
1340            lookback,
1341            signal_length,
1342        )
1343        .map(|result| (result, ctx, dev_id))
1344        .map_err(|e| PyValueError::new_err(e.to_string()))
1345    })?;
1346
1347    dict.set_item(
1348        "rs",
1349        DeviceArrayF32Py {
1350            inner: result.rs,
1351            _ctx: Some(ctx.clone()),
1352            device_id: Some(dev_id),
1353        },
1354    )?;
1355    dict.set_item(
1356        "signal",
1357        DeviceArrayF32Py {
1358            inner: result.signal,
1359            _ctx: Some(ctx),
1360            device_id: Some(dev_id),
1361        },
1362    )?;
1363    dict.set_item("rows", rows)?;
1364    dict.set_item("cols", cols)?;
1365    Ok(dict)
1366}
1367
1368#[cfg(feature = "python")]
1369pub fn register_rogers_satchell_volatility_module(
1370    m: &Bound<'_, pyo3::types::PyModule>,
1371) -> PyResult<()> {
1372    m.add_function(wrap_pyfunction!(rogers_satchell_volatility_py, m)?)?;
1373    m.add_function(wrap_pyfunction!(rogers_satchell_volatility_batch_py, m)?)?;
1374    m.add_class::<RogersSatchellVolatilityStreamPy>()?;
1375
1376    #[cfg(feature = "cuda")]
1377    {
1378        m.add_class::<DeviceArrayF32Py>()?;
1379        m.add_function(wrap_pyfunction!(
1380            rogers_satchell_volatility_cuda_batch_dev_py,
1381            m
1382        )?)?;
1383        m.add_function(wrap_pyfunction!(
1384            rogers_satchell_volatility_cuda_many_series_one_param_dev_py,
1385            m
1386        )?)?;
1387    }
1388    Ok(())
1389}
1390
1391#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1392#[wasm_bindgen(js_name = "rogers_satchell_volatility_js")]
1393pub fn rogers_satchell_volatility_js(
1394    open: &[f64],
1395    high: &[f64],
1396    low: &[f64],
1397    close: &[f64],
1398    lookback: usize,
1399    signal_length: usize,
1400) -> Result<JsValue, JsValue> {
1401    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1402        return Err(JsValue::from_str("OHLC slice length mismatch"));
1403    }
1404
1405    let input = RogersSatchellVolatilityInput::from_slices(
1406        open,
1407        high,
1408        low,
1409        close,
1410        RogersSatchellVolatilityParams {
1411            lookback: Some(lookback),
1412            signal_length: Some(signal_length),
1413        },
1414    );
1415    let mut rs = vec![0.0; close.len()];
1416    let mut signal = vec![0.0; close.len()];
1417    rogers_satchell_volatility_into_slice(&mut rs, &mut signal, &input, Kernel::Auto)
1418        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1419
1420    let obj = js_sys::Object::new();
1421    js_sys::Reflect::set(
1422        &obj,
1423        &JsValue::from_str("rs"),
1424        &serde_wasm_bindgen::to_value(&rs).unwrap(),
1425    )?;
1426    js_sys::Reflect::set(
1427        &obj,
1428        &JsValue::from_str("signal"),
1429        &serde_wasm_bindgen::to_value(&signal).unwrap(),
1430    )?;
1431    Ok(obj.into())
1432}
1433
1434#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1435#[derive(Serialize, Deserialize)]
1436pub struct RogersSatchellVolatilityBatchConfig {
1437    pub lookback_range: Vec<usize>,
1438    pub signal_length_range: Vec<usize>,
1439}
1440
1441#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1442#[wasm_bindgen(js_name = "rogers_satchell_volatility_batch_js")]
1443pub fn rogers_satchell_volatility_batch_js(
1444    open: &[f64],
1445    high: &[f64],
1446    low: &[f64],
1447    close: &[f64],
1448    config: JsValue,
1449) -> Result<JsValue, JsValue> {
1450    if open.len() != high.len() || open.len() != low.len() || open.len() != close.len() {
1451        return Err(JsValue::from_str("OHLC slice length mismatch"));
1452    }
1453
1454    let config: RogersSatchellVolatilityBatchConfig = serde_wasm_bindgen::from_value(config)
1455        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1456    if config.lookback_range.len() != 3 {
1457        return Err(JsValue::from_str(
1458            "Invalid config: lookback_range must have exactly 3 elements [start, end, step]",
1459        ));
1460    }
1461    if config.signal_length_range.len() != 3 {
1462        return Err(JsValue::from_str(
1463            "Invalid config: signal_length_range must have exactly 3 elements [start, end, step]",
1464        ));
1465    }
1466
1467    let sweep = RogersSatchellVolatilityBatchRange {
1468        lookback: (
1469            config.lookback_range[0],
1470            config.lookback_range[1],
1471            config.lookback_range[2],
1472        ),
1473        signal_length: (
1474            config.signal_length_range[0],
1475            config.signal_length_range[1],
1476            config.signal_length_range[2],
1477        ),
1478    };
1479    let combos =
1480        expand_grid_rogers_satchell(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1481    let rows = combos.len();
1482    let cols = close.len();
1483    let total = rows
1484        .checked_mul(cols)
1485        .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1486
1487    let mut rs = vec![0.0; total];
1488    let mut signal = vec![0.0; total];
1489    rogers_satchell_volatility_batch_inner_into(
1490        open,
1491        high,
1492        low,
1493        close,
1494        &sweep,
1495        detect_best_kernel(),
1496        false,
1497        &mut rs,
1498        &mut signal,
1499    )
1500    .map_err(|e| JsValue::from_str(&e.to_string()))?;
1501
1502    let obj = js_sys::Object::new();
1503    js_sys::Reflect::set(
1504        &obj,
1505        &JsValue::from_str("rs"),
1506        &serde_wasm_bindgen::to_value(&rs).unwrap(),
1507    )?;
1508    js_sys::Reflect::set(
1509        &obj,
1510        &JsValue::from_str("signal"),
1511        &serde_wasm_bindgen::to_value(&signal).unwrap(),
1512    )?;
1513    js_sys::Reflect::set(
1514        &obj,
1515        &JsValue::from_str("rows"),
1516        &JsValue::from_f64(rows as f64),
1517    )?;
1518    js_sys::Reflect::set(
1519        &obj,
1520        &JsValue::from_str("cols"),
1521        &JsValue::from_f64(cols as f64),
1522    )?;
1523    js_sys::Reflect::set(
1524        &obj,
1525        &JsValue::from_str("combos"),
1526        &serde_wasm_bindgen::to_value(&combos).unwrap(),
1527    )?;
1528    Ok(obj.into())
1529}
1530
1531#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1532#[wasm_bindgen]
1533pub fn rogers_satchell_volatility_alloc(len: usize) -> *mut f64 {
1534    let mut values = Vec::<f64>::with_capacity(2 * len);
1535    let ptr = values.as_mut_ptr();
1536    std::mem::forget(values);
1537    ptr
1538}
1539
1540#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1541#[wasm_bindgen]
1542pub fn rogers_satchell_volatility_free(ptr: *mut f64, len: usize) {
1543    unsafe {
1544        let _ = Vec::from_raw_parts(ptr, 2 * len, 2 * len);
1545    }
1546}
1547
1548#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1549#[wasm_bindgen]
1550pub fn rogers_satchell_volatility_into(
1551    open_ptr: *const f64,
1552    high_ptr: *const f64,
1553    low_ptr: *const f64,
1554    close_ptr: *const f64,
1555    out_ptr: *mut f64,
1556    len: usize,
1557    lookback: usize,
1558    signal_length: usize,
1559) -> Result<(), JsValue> {
1560    if open_ptr.is_null()
1561        || high_ptr.is_null()
1562        || low_ptr.is_null()
1563        || close_ptr.is_null()
1564        || out_ptr.is_null()
1565    {
1566        return Err(JsValue::from_str(
1567            "null pointer passed to rogers_satchell_volatility_into",
1568        ));
1569    }
1570
1571    unsafe {
1572        let open = std::slice::from_raw_parts(open_ptr, len);
1573        let high = std::slice::from_raw_parts(high_ptr, len);
1574        let low = std::slice::from_raw_parts(low_ptr, len);
1575        let close = std::slice::from_raw_parts(close_ptr, len);
1576        let out = std::slice::from_raw_parts_mut(out_ptr, 2 * len);
1577        let (rs, signal) = out.split_at_mut(len);
1578
1579        let input = RogersSatchellVolatilityInput::from_slices(
1580            open,
1581            high,
1582            low,
1583            close,
1584            RogersSatchellVolatilityParams {
1585                lookback: Some(lookback),
1586                signal_length: Some(signal_length),
1587            },
1588        );
1589        rogers_satchell_volatility_into_slice(rs, signal, &input, Kernel::Auto)
1590            .map_err(|e| JsValue::from_str(&e.to_string()))
1591    }
1592}
1593
1594#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1595#[wasm_bindgen]
1596pub fn rogers_satchell_volatility_batch_into(
1597    open_ptr: *const f64,
1598    high_ptr: *const f64,
1599    low_ptr: *const f64,
1600    close_ptr: *const f64,
1601    rs_ptr: *mut f64,
1602    signal_ptr: *mut f64,
1603    len: usize,
1604    lookback_start: usize,
1605    lookback_end: usize,
1606    lookback_step: usize,
1607    signal_length_start: usize,
1608    signal_length_end: usize,
1609    signal_length_step: usize,
1610) -> Result<usize, JsValue> {
1611    if open_ptr.is_null()
1612        || high_ptr.is_null()
1613        || low_ptr.is_null()
1614        || close_ptr.is_null()
1615        || rs_ptr.is_null()
1616        || signal_ptr.is_null()
1617    {
1618        return Err(JsValue::from_str("null pointer"));
1619    }
1620
1621    unsafe {
1622        let open = std::slice::from_raw_parts(open_ptr, len);
1623        let high = std::slice::from_raw_parts(high_ptr, len);
1624        let low = std::slice::from_raw_parts(low_ptr, len);
1625        let close = std::slice::from_raw_parts(close_ptr, len);
1626
1627        let sweep = RogersSatchellVolatilityBatchRange {
1628            lookback: (lookback_start, lookback_end, lookback_step),
1629            signal_length: (signal_length_start, signal_length_end, signal_length_step),
1630        };
1631        let combos =
1632            expand_grid_rogers_satchell(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1633        let rows = combos.len();
1634        let cols = len;
1635        let rs_out = std::slice::from_raw_parts_mut(rs_ptr, rows * cols);
1636        let signal_out = std::slice::from_raw_parts_mut(signal_ptr, rows * cols);
1637
1638        rogers_satchell_volatility_batch_inner_into(
1639            open,
1640            high,
1641            low,
1642            close,
1643            &sweep,
1644            detect_best_kernel(),
1645            false,
1646            rs_out,
1647            signal_out,
1648        )
1649        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1650        Ok(rows)
1651    }
1652}
1653
1654#[cfg(test)]
1655mod tests {
1656    use super::*;
1657    use std::error::Error;
1658
1659    fn sample_ohlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1660        let mut open = vec![0.0; len];
1661        let mut high = vec![0.0; len];
1662        let mut low = vec![0.0; len];
1663        let mut close = vec![0.0; len];
1664        let mut prev = 100.0;
1665        for i in 0..len {
1666            let x = i as f64;
1667            let o = (prev + (x * 0.17).sin() * 0.9 + 0.03 * x).max(1.0);
1668            let c = (o + (x * 0.11).cos() * 0.7).max(1.0);
1669            let h = o.max(c) + 0.4 + (x * 0.07).cos().abs() * 0.1;
1670            let l = (o.min(c) - 0.35 - (x * 0.13).sin().abs() * 0.08).max(0.01);
1671            open[i] = o;
1672            high[i] = h;
1673            low[i] = l;
1674            close[i] = c.max(0.01);
1675            prev = close[i];
1676        }
1677        (open, high, low, close)
1678    }
1679
1680    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
1681        if a.is_nan() && b.is_nan() {
1682            return true;
1683        }
1684        (a - b).abs() <= tol
1685    }
1686
1687    #[test]
1688    fn test_output_contract() -> Result<(), Box<dyn Error>> {
1689        let (open, high, low, close) = sample_ohlc(128);
1690        let input = RogersSatchellVolatilityInput::from_slices(
1691            &open,
1692            &high,
1693            &low,
1694            &close,
1695            RogersSatchellVolatilityParams {
1696                lookback: Some(8),
1697                signal_length: Some(8),
1698            },
1699        );
1700        let out = rogers_satchell_volatility(&input)?;
1701        assert_eq!(out.rs.len(), close.len());
1702        assert_eq!(out.signal.len(), close.len());
1703        assert!(out.rs[..7].iter().all(|v| v.is_nan()));
1704        assert!(out.signal[..14].iter().all(|v| v.is_nan()));
1705        assert!(out.rs.iter().skip(32).all(|v| v.is_finite()));
1706        assert!(out.signal.iter().skip(48).all(|v| v.is_finite()));
1707        Ok(())
1708    }
1709
1710    #[test]
1711    fn test_into_slice_matches_safe_api() -> Result<(), Box<dyn Error>> {
1712        let (open, high, low, close) = sample_ohlc(96);
1713        let input = RogersSatchellVolatilityInput::from_slices(
1714            &open,
1715            &high,
1716            &low,
1717            &close,
1718            RogersSatchellVolatilityParams {
1719                lookback: Some(10),
1720                signal_length: Some(6),
1721            },
1722        );
1723        let baseline = rogers_satchell_volatility_with_kernel(&input, Kernel::Scalar)?;
1724        let mut rs = vec![0.0; close.len()];
1725        let mut signal = vec![0.0; close.len()];
1726        rogers_satchell_volatility_into_slice(&mut rs, &mut signal, &input, Kernel::Auto)?;
1727        for i in 0..close.len() {
1728            assert!(approx_eq(baseline.rs[i], rs[i], 1e-12));
1729            assert!(approx_eq(baseline.signal[i], signal[i], 1e-12));
1730        }
1731        Ok(())
1732    }
1733
1734    #[test]
1735    fn test_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1736        let (open, high, low, close) = sample_ohlc(96);
1737        let input = RogersSatchellVolatilityInput::from_slices(
1738            &open,
1739            &high,
1740            &low,
1741            &close,
1742            RogersSatchellVolatilityParams {
1743                lookback: Some(9),
1744                signal_length: Some(5),
1745            },
1746        );
1747        let batch = rogers_satchell_volatility(&input)?;
1748        let mut stream = RogersSatchellVolatilityStream::try_new(RogersSatchellVolatilityParams {
1749            lookback: Some(9),
1750            signal_length: Some(5),
1751        })?;
1752        let mut rs = Vec::with_capacity(close.len());
1753        let mut signal = Vec::with_capacity(close.len());
1754        for i in 0..close.len() {
1755            if let Some((r, s)) = stream.update(open[i], high[i], low[i], close[i]) {
1756                rs.push(r);
1757                signal.push(s);
1758            } else {
1759                rs.push(f64::NAN);
1760                signal.push(f64::NAN);
1761            }
1762        }
1763        for i in 0..close.len() {
1764            assert!(approx_eq(batch.rs[i], rs[i], 1e-12));
1765            assert!(approx_eq(batch.signal[i], signal[i], 1e-12));
1766        }
1767        Ok(())
1768    }
1769
1770    #[test]
1771    fn test_batch_single_param_matches_single() -> Result<(), Box<dyn Error>> {
1772        let (open, high, low, close) = sample_ohlc(128);
1773        let sweep = RogersSatchellVolatilityBatchRange {
1774            lookback: (12, 12, 0),
1775            signal_length: (5, 5, 0),
1776        };
1777        let batch = rogers_satchell_volatility_batch_with_kernel(
1778            &open,
1779            &high,
1780            &low,
1781            &close,
1782            &sweep,
1783            Kernel::ScalarBatch,
1784        )?;
1785        assert_eq!(batch.rows, 1);
1786        assert_eq!(batch.cols, close.len());
1787        let input = RogersSatchellVolatilityInput::from_slices(
1788            &open,
1789            &high,
1790            &low,
1791            &close,
1792            RogersSatchellVolatilityParams {
1793                lookback: Some(12),
1794                signal_length: Some(5),
1795            },
1796        );
1797        let single = rogers_satchell_volatility_with_kernel(&input, Kernel::Scalar)?;
1798        for i in 0..close.len() {
1799            assert!(approx_eq(batch.rs[i], single.rs[i], 1e-12));
1800            assert!(approx_eq(batch.signal[i], single.signal[i], 1e-12));
1801        }
1802        Ok(())
1803    }
1804
1805    #[test]
1806    fn test_validation_errors() {
1807        let (open, high, low, close) = sample_ohlc(16);
1808        let input = RogersSatchellVolatilityInput::from_slices(
1809            &open,
1810            &high,
1811            &low,
1812            &close,
1813            RogersSatchellVolatilityParams {
1814                lookback: Some(0),
1815                signal_length: Some(8),
1816            },
1817        );
1818        assert!(matches!(
1819            rogers_satchell_volatility(&input),
1820            Err(RogersSatchellVolatilityError::InvalidLookback { .. })
1821        ));
1822
1823        let input = RogersSatchellVolatilityInput::from_slices(
1824            &open,
1825            &high,
1826            &low,
1827            &close,
1828            RogersSatchellVolatilityParams {
1829                lookback: Some(8),
1830                signal_length: Some(0),
1831            },
1832        );
1833        assert!(matches!(
1834            rogers_satchell_volatility(&input),
1835            Err(RogersSatchellVolatilityError::InvalidSignalLength { .. })
1836        ));
1837    }
1838}