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}