1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::mem::ManuallyDrop;
27use thiserror::Error;
28
29#[derive(Debug, Clone)]
30pub enum GopalakrishnanRangeIndexData<'a> {
31 Candles { candles: &'a Candles },
32 Slices { high: &'a [f64], low: &'a [f64] },
33}
34
35#[derive(Debug, Clone)]
36pub struct GopalakrishnanRangeIndexOutput {
37 pub values: Vec<f64>,
38}
39
40#[derive(Debug, Clone)]
41#[cfg_attr(
42 all(target_arch = "wasm32", feature = "wasm"),
43 derive(Serialize, Deserialize)
44)]
45pub struct GopalakrishnanRangeIndexParams {
46 pub length: Option<usize>,
47}
48
49impl Default for GopalakrishnanRangeIndexParams {
50 fn default() -> Self {
51 Self { length: Some(5) }
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct GopalakrishnanRangeIndexInput<'a> {
57 pub data: GopalakrishnanRangeIndexData<'a>,
58 pub params: GopalakrishnanRangeIndexParams,
59}
60
61impl<'a> GopalakrishnanRangeIndexInput<'a> {
62 #[inline]
63 pub fn from_candles(candles: &'a Candles, params: GopalakrishnanRangeIndexParams) -> Self {
64 Self {
65 data: GopalakrishnanRangeIndexData::Candles { candles },
66 params,
67 }
68 }
69
70 #[inline]
71 pub fn from_slices(
72 high: &'a [f64],
73 low: &'a [f64],
74 params: GopalakrishnanRangeIndexParams,
75 ) -> Self {
76 Self {
77 data: GopalakrishnanRangeIndexData::Slices { high, low },
78 params,
79 }
80 }
81
82 #[inline]
83 pub fn with_default_candles(candles: &'a Candles) -> Self {
84 Self::from_candles(candles, GopalakrishnanRangeIndexParams::default())
85 }
86
87 #[inline]
88 pub fn get_length(&self) -> usize {
89 self.params.length.unwrap_or(5)
90 }
91}
92
93#[derive(Copy, Clone, Debug)]
94pub struct GopalakrishnanRangeIndexBuilder {
95 length: Option<usize>,
96 kernel: Kernel,
97}
98
99impl Default for GopalakrishnanRangeIndexBuilder {
100 fn default() -> Self {
101 Self {
102 length: None,
103 kernel: Kernel::Auto,
104 }
105 }
106}
107
108impl GopalakrishnanRangeIndexBuilder {
109 #[inline(always)]
110 pub fn new() -> Self {
111 Self::default()
112 }
113
114 #[inline(always)]
115 pub fn length(mut self, length: usize) -> Self {
116 self.length = Some(length);
117 self
118 }
119
120 #[inline(always)]
121 pub fn kernel(mut self, kernel: Kernel) -> Self {
122 self.kernel = kernel;
123 self
124 }
125
126 #[inline(always)]
127 pub fn apply(
128 self,
129 candles: &Candles,
130 ) -> Result<GopalakrishnanRangeIndexOutput, GopalakrishnanRangeIndexError> {
131 let input = GopalakrishnanRangeIndexInput::from_candles(
132 candles,
133 GopalakrishnanRangeIndexParams {
134 length: self.length,
135 },
136 );
137 gopalakrishnan_range_index_with_kernel(&input, self.kernel)
138 }
139
140 #[inline(always)]
141 pub fn apply_slices(
142 self,
143 high: &[f64],
144 low: &[f64],
145 ) -> Result<GopalakrishnanRangeIndexOutput, GopalakrishnanRangeIndexError> {
146 let input = GopalakrishnanRangeIndexInput::from_slices(
147 high,
148 low,
149 GopalakrishnanRangeIndexParams {
150 length: self.length,
151 },
152 );
153 gopalakrishnan_range_index_with_kernel(&input, self.kernel)
154 }
155
156 #[inline(always)]
157 pub fn into_stream(
158 self,
159 ) -> Result<GopalakrishnanRangeIndexStream, GopalakrishnanRangeIndexError> {
160 GopalakrishnanRangeIndexStream::try_new(GopalakrishnanRangeIndexParams {
161 length: self.length,
162 })
163 }
164}
165
166#[derive(Debug, Error)]
167pub enum GopalakrishnanRangeIndexError {
168 #[error("gopalakrishnan_range_index: Input data slice is empty.")]
169 EmptyInputData,
170 #[error("gopalakrishnan_range_index: All values are NaN.")]
171 AllValuesNaN,
172 #[error(
173 "gopalakrishnan_range_index: Invalid length: length = {length}, data length = {data_len}. Length must be > 1."
174 )]
175 InvalidLength { length: usize, data_len: usize },
176 #[error(
177 "gopalakrishnan_range_index: Not enough valid data: needed = {needed}, valid = {valid}"
178 )]
179 NotEnoughValidData { needed: usize, valid: usize },
180 #[error(
181 "gopalakrishnan_range_index: Inconsistent slice lengths: high={high_len}, low={low_len}"
182 )]
183 InconsistentSliceLengths { high_len: usize, low_len: usize },
184 #[error(
185 "gopalakrishnan_range_index: Output length mismatch: expected = {expected}, got = {got}"
186 )]
187 OutputLengthMismatch { expected: usize, got: usize },
188 #[error("gopalakrishnan_range_index: Invalid range: start={start}, end={end}, step={step}")]
189 InvalidRange {
190 start: String,
191 end: String,
192 step: String,
193 },
194 #[error("gopalakrishnan_range_index: Invalid kernel for batch: {0:?}")]
195 InvalidKernelForBatch(Kernel),
196}
197
198#[derive(Debug, Clone)]
199pub struct GopalakrishnanRangeIndexStream {
200 length: usize,
201 seen: usize,
202 started: bool,
203 dq_high: VecDeque<(usize, f64)>,
204 dq_low: VecDeque<(usize, f64)>,
205 valid: Vec<u8>,
206 valid_idx: usize,
207 valid_cnt: usize,
208 total_cnt: usize,
209 log_length: f64,
210}
211
212impl GopalakrishnanRangeIndexStream {
213 pub fn try_new(
214 params: GopalakrishnanRangeIndexParams,
215 ) -> Result<GopalakrishnanRangeIndexStream, GopalakrishnanRangeIndexError> {
216 let length = params.length.unwrap_or(5);
217 if length <= 1 {
218 return Err(GopalakrishnanRangeIndexError::InvalidLength {
219 length,
220 data_len: 0,
221 });
222 }
223 Ok(Self {
224 length,
225 seen: 0,
226 started: false,
227 dq_high: VecDeque::with_capacity(length + 1),
228 dq_low: VecDeque::with_capacity(length + 1),
229 valid: vec![0u8; length],
230 valid_idx: 0,
231 valid_cnt: 0,
232 total_cnt: 0,
233 log_length: (length as f64).ln(),
234 })
235 }
236
237 #[inline(always)]
238 pub fn update(&mut self, high: f64, low: f64) -> Option<f64> {
239 if !self.started {
240 if !valid_high_low_bar(high, low) {
241 return None;
242 }
243 self.started = true;
244 }
245
246 if self.total_cnt >= self.length {
247 let old_idx = self.valid_idx;
248 if self.valid[old_idx] != 0 {
249 self.valid_cnt = self.valid_cnt.saturating_sub(1);
250 }
251 } else {
252 self.total_cnt += 1;
253 }
254
255 let i = self.seen;
256 if valid_high_low_bar(high, low) {
257 while let Some(&(_, back)) = self.dq_high.back() {
258 if back <= high {
259 self.dq_high.pop_back();
260 } else {
261 break;
262 }
263 }
264 self.dq_high.push_back((i, high));
265
266 while let Some(&(_, back)) = self.dq_low.back() {
267 if back >= low {
268 self.dq_low.pop_back();
269 } else {
270 break;
271 }
272 }
273 self.dq_low.push_back((i, low));
274
275 self.valid[self.valid_idx] = 1;
276 self.valid_cnt += 1;
277 } else {
278 self.valid[self.valid_idx] = 0;
279 }
280
281 self.valid_idx += 1;
282 if self.valid_idx == self.length {
283 self.valid_idx = 0;
284 }
285
286 let start = i.saturating_add(1).saturating_sub(self.length);
287 while let Some(&(idx, _)) = self.dq_high.front() {
288 if idx < start {
289 self.dq_high.pop_front();
290 } else {
291 break;
292 }
293 }
294 while let Some(&(idx, _)) = self.dq_low.front() {
295 if idx < start {
296 self.dq_low.pop_front();
297 } else {
298 break;
299 }
300 }
301
302 self.seen = i + 1;
303 if self.total_cnt < self.length {
304 return None;
305 }
306 if self.valid_cnt != self.length {
307 return Some(f64::NAN);
308 }
309
310 let highest = self
311 .dq_high
312 .front()
313 .map(|&(_, v)| v)
314 .unwrap_or(f64::NEG_INFINITY);
315 let lowest = self
316 .dq_low
317 .front()
318 .map(|&(_, v)| v)
319 .unwrap_or(f64::INFINITY);
320 Some(gapo_value(highest, lowest, self.log_length))
321 }
322
323 #[inline(always)]
324 pub fn get_warmup_period(&self) -> usize {
325 self.length.saturating_sub(1)
326 }
327}
328
329#[inline]
330pub fn gopalakrishnan_range_index(
331 input: &GopalakrishnanRangeIndexInput,
332) -> Result<GopalakrishnanRangeIndexOutput, GopalakrishnanRangeIndexError> {
333 gopalakrishnan_range_index_with_kernel(input, Kernel::Auto)
334}
335
336#[inline(always)]
337fn valid_high_low_bar(high: f64, low: f64) -> bool {
338 high.is_finite() && low.is_finite()
339}
340
341#[inline(always)]
342fn gapo_value(highest: f64, lowest: f64, log_length: f64) -> f64 {
343 let range = highest - lowest;
344 if range.is_finite() && range > 0.0 {
345 range.ln() / log_length
346 } else {
347 f64::NAN
348 }
349}
350
351#[inline(always)]
352fn first_valid_high_low(high: &[f64], low: &[f64]) -> usize {
353 let len = high.len();
354 let mut i = 0usize;
355 while i < len {
356 if valid_high_low_bar(high[i], low[i]) {
357 break;
358 }
359 i += 1;
360 }
361 i.min(len)
362}
363
364#[inline(always)]
365fn count_valid_high_low(high: &[f64], low: &[f64]) -> usize {
366 let mut count = 0usize;
367 for i in 0..high.len() {
368 if valid_high_low_bar(high[i], low[i]) {
369 count += 1;
370 }
371 }
372 count
373}
374
375#[inline(always)]
376fn build_prefix_valid(high: &[f64], low: &[f64]) -> Vec<u32> {
377 let len = high.len();
378 let mut prefix_valid = vec![0u32; len + 1];
379 for i in 0..len {
380 prefix_valid[i + 1] = prefix_valid[i]
381 + if valid_high_low_bar(high[i], low[i]) {
382 1
383 } else {
384 0
385 };
386 }
387 prefix_valid
388}
389
390#[inline(always)]
391fn gapo_row_from_slices(
392 high: &[f64],
393 low: &[f64],
394 prefix_valid: &[u32],
395 length: usize,
396 first: usize,
397 out: &mut [f64],
398) {
399 out.fill(f64::NAN);
400
401 let warmup = first.saturating_add(length.saturating_sub(1));
402 let length_u32 = length as u32;
403 let log_length = (length as f64).ln();
404 let mut dq_high = VecDeque::<usize>::with_capacity(length + 1);
405 let mut dq_low = VecDeque::<usize>::with_capacity(length + 1);
406
407 for i in first..high.len() {
408 if valid_high_low_bar(high[i], low[i]) {
409 while let Some(&j) = dq_high.back() {
410 if high[j] <= high[i] {
411 dq_high.pop_back();
412 } else {
413 break;
414 }
415 }
416 dq_high.push_back(i);
417
418 while let Some(&j) = dq_low.back() {
419 if low[j] >= low[i] {
420 dq_low.pop_back();
421 } else {
422 break;
423 }
424 }
425 dq_low.push_back(i);
426 }
427
428 if i < warmup {
429 continue;
430 }
431
432 let start = i + 1 - length;
433 while let Some(&j) = dq_high.front() {
434 if j < start {
435 dq_high.pop_front();
436 } else {
437 break;
438 }
439 }
440 while let Some(&j) = dq_low.front() {
441 if j < start {
442 dq_low.pop_front();
443 } else {
444 break;
445 }
446 }
447
448 let valid_count = prefix_valid[i + 1] - prefix_valid[start];
449 if valid_count != length_u32 {
450 continue;
451 }
452
453 let highest = dq_high
454 .front()
455 .map(|&j| high[j])
456 .unwrap_or(f64::NEG_INFINITY);
457 let lowest = dq_low.front().map(|&j| low[j]).unwrap_or(f64::INFINITY);
458 out[i] = gapo_value(highest, lowest, log_length);
459 }
460}
461
462#[inline(always)]
463fn gopalakrishnan_range_index_prepare<'a>(
464 input: &'a GopalakrishnanRangeIndexInput,
465 kernel: Kernel,
466) -> Result<(&'a [f64], &'a [f64], usize, usize, Kernel), GopalakrishnanRangeIndexError> {
467 let (high, low): (&[f64], &[f64]) = match &input.data {
468 GopalakrishnanRangeIndexData::Candles { candles } => (&candles.high, &candles.low),
469 GopalakrishnanRangeIndexData::Slices { high, low } => (high, low),
470 };
471
472 let len = high.len();
473 if len == 0 {
474 return Err(GopalakrishnanRangeIndexError::EmptyInputData);
475 }
476 if low.len() != len {
477 return Err(GopalakrishnanRangeIndexError::InconsistentSliceLengths {
478 high_len: high.len(),
479 low_len: low.len(),
480 });
481 }
482
483 let first = first_valid_high_low(high, low);
484 if first >= len {
485 return Err(GopalakrishnanRangeIndexError::AllValuesNaN);
486 }
487
488 let length = input.get_length();
489 if length <= 1 || length > len {
490 return Err(GopalakrishnanRangeIndexError::InvalidLength {
491 length,
492 data_len: len,
493 });
494 }
495
496 let valid = count_valid_high_low(high, low);
497 if valid < length {
498 return Err(GopalakrishnanRangeIndexError::NotEnoughValidData {
499 needed: length,
500 valid,
501 });
502 }
503
504 let chosen = match kernel {
505 Kernel::Auto => detect_best_kernel(),
506 other => other.to_non_batch(),
507 };
508
509 Ok((high, low, length, first, chosen))
510}
511
512#[inline]
513pub fn gopalakrishnan_range_index_with_kernel(
514 input: &GopalakrishnanRangeIndexInput,
515 kernel: Kernel,
516) -> Result<GopalakrishnanRangeIndexOutput, GopalakrishnanRangeIndexError> {
517 let (high, low, length, first, _chosen) = gopalakrishnan_range_index_prepare(input, kernel)?;
518 let mut values =
519 alloc_with_nan_prefix(high.len(), first.saturating_add(length.saturating_sub(1)));
520 let prefix_valid = build_prefix_valid(high, low);
521 gapo_row_from_slices(high, low, &prefix_valid, length, first, &mut values);
522 Ok(GopalakrishnanRangeIndexOutput { values })
523}
524
525#[inline]
526pub fn gopalakrishnan_range_index_into_slice(
527 dst: &mut [f64],
528 input: &GopalakrishnanRangeIndexInput,
529 kernel: Kernel,
530) -> Result<(), GopalakrishnanRangeIndexError> {
531 let (high, low, length, first, _chosen) = gopalakrishnan_range_index_prepare(input, kernel)?;
532 if dst.len() != high.len() {
533 return Err(GopalakrishnanRangeIndexError::OutputLengthMismatch {
534 expected: high.len(),
535 got: dst.len(),
536 });
537 }
538 let prefix_valid = build_prefix_valid(high, low);
539 gapo_row_from_slices(high, low, &prefix_valid, length, first, dst);
540 Ok(())
541}
542
543#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
544#[inline]
545pub fn gopalakrishnan_range_index_into(
546 input: &GopalakrishnanRangeIndexInput,
547 out: &mut [f64],
548) -> Result<(), GopalakrishnanRangeIndexError> {
549 gopalakrishnan_range_index_into_slice(out, input, Kernel::Auto)
550}
551
552#[derive(Clone, Debug)]
553pub struct GopalakrishnanRangeIndexBatchRange {
554 pub length: (usize, usize, usize),
555}
556
557impl Default for GopalakrishnanRangeIndexBatchRange {
558 fn default() -> Self {
559 Self {
560 length: (5, 252, 1),
561 }
562 }
563}
564
565#[derive(Clone, Debug, Default)]
566pub struct GopalakrishnanRangeIndexBatchBuilder {
567 range: GopalakrishnanRangeIndexBatchRange,
568 kernel: Kernel,
569}
570
571impl GopalakrishnanRangeIndexBatchBuilder {
572 #[inline]
573 pub fn new() -> Self {
574 Self::default()
575 }
576
577 #[inline]
578 pub fn kernel(mut self, kernel: Kernel) -> Self {
579 self.kernel = kernel;
580 self
581 }
582
583 #[inline]
584 pub fn length_range(mut self, start: usize, end: usize, step: usize) -> Self {
585 self.range.length = (start, end, step);
586 self
587 }
588
589 #[inline]
590 pub fn length_static(mut self, length: usize) -> Self {
591 self.range.length = (length, length, 0);
592 self
593 }
594
595 #[inline]
596 pub fn apply_slices(
597 self,
598 high: &[f64],
599 low: &[f64],
600 ) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
601 gopalakrishnan_range_index_batch_with_kernel(high, low, &self.range, self.kernel)
602 }
603
604 #[inline]
605 pub fn apply_candles(
606 self,
607 candles: &Candles,
608 ) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
609 self.apply_slices(&candles.high, &candles.low)
610 }
611
612 #[inline]
613 pub fn with_default_candles(
614 candles: &Candles,
615 ) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
616 GopalakrishnanRangeIndexBatchBuilder::new()
617 .kernel(Kernel::Auto)
618 .apply_candles(candles)
619 }
620}
621
622#[derive(Clone, Debug)]
623pub struct GopalakrishnanRangeIndexBatchOutput {
624 pub values: Vec<f64>,
625 pub combos: Vec<GopalakrishnanRangeIndexParams>,
626 pub rows: usize,
627 pub cols: usize,
628}
629
630impl GopalakrishnanRangeIndexBatchOutput {
631 pub fn row_for_params(&self, params: &GopalakrishnanRangeIndexParams) -> Option<usize> {
632 self.combos
633 .iter()
634 .position(|combo| combo.length.unwrap_or(5) == params.length.unwrap_or(5))
635 }
636
637 pub fn values_for(&self, params: &GopalakrishnanRangeIndexParams) -> Option<&[f64]> {
638 self.row_for_params(params).and_then(|row| {
639 row.checked_mul(self.cols)
640 .and_then(|start| self.values.get(start..start + self.cols))
641 })
642 }
643}
644
645#[inline(always)]
646fn expand_grid_gopalakrishnan_range_index(
647 range: &GopalakrishnanRangeIndexBatchRange,
648) -> Result<Vec<GopalakrishnanRangeIndexParams>, GopalakrishnanRangeIndexError> {
649 let (start, end, step) = range.length;
650 let lengths = if step == 0 || start == end {
651 vec![start]
652 } else if start < end {
653 let mut out = Vec::new();
654 let mut x = start;
655 let st = step.max(1);
656 while x <= end {
657 out.push(x);
658 let next = x.saturating_add(st);
659 if next == x {
660 break;
661 }
662 x = next;
663 }
664 out
665 } else {
666 let mut out = Vec::new();
667 let mut x = start;
668 let st = step.max(1);
669 loop {
670 out.push(x);
671 if x == end {
672 break;
673 }
674 let next = x.saturating_sub(st);
675 if next == x || next < end {
676 break;
677 }
678 x = next;
679 }
680 out
681 };
682
683 if lengths.is_empty() {
684 return Err(GopalakrishnanRangeIndexError::InvalidRange {
685 start: start.to_string(),
686 end: end.to_string(),
687 step: step.to_string(),
688 });
689 }
690 if let Some(&bad) = lengths.iter().find(|&&length| length <= 1) {
691 return Err(GopalakrishnanRangeIndexError::InvalidLength {
692 length: bad,
693 data_len: 0,
694 });
695 }
696
697 Ok(lengths
698 .into_iter()
699 .map(|length| GopalakrishnanRangeIndexParams {
700 length: Some(length),
701 })
702 .collect())
703}
704
705#[inline]
706pub fn gopalakrishnan_range_index_batch_with_kernel(
707 high: &[f64],
708 low: &[f64],
709 sweep: &GopalakrishnanRangeIndexBatchRange,
710 kernel: Kernel,
711) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
712 let batch_kernel = match kernel {
713 Kernel::Auto => detect_best_batch_kernel(),
714 other if other.is_batch() => other,
715 other => return Err(GopalakrishnanRangeIndexError::InvalidKernelForBatch(other)),
716 };
717 gopalakrishnan_range_index_batch_par_slice(high, low, sweep, batch_kernel.to_non_batch())
718}
719
720#[inline]
721pub fn gopalakrishnan_range_index_batch_slice(
722 high: &[f64],
723 low: &[f64],
724 sweep: &GopalakrishnanRangeIndexBatchRange,
725 kernel: Kernel,
726) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
727 gopalakrishnan_range_index_batch_inner(high, low, sweep, kernel, false)
728}
729
730#[inline]
731pub fn gopalakrishnan_range_index_batch_par_slice(
732 high: &[f64],
733 low: &[f64],
734 sweep: &GopalakrishnanRangeIndexBatchRange,
735 kernel: Kernel,
736) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
737 gopalakrishnan_range_index_batch_inner(high, low, sweep, kernel, true)
738}
739
740#[inline(always)]
741fn gopalakrishnan_range_index_batch_inner(
742 high: &[f64],
743 low: &[f64],
744 sweep: &GopalakrishnanRangeIndexBatchRange,
745 _kernel: Kernel,
746 parallel: bool,
747) -> Result<GopalakrishnanRangeIndexBatchOutput, GopalakrishnanRangeIndexError> {
748 let combos = expand_grid_gopalakrishnan_range_index(sweep)?;
749 let rows = combos.len();
750 let cols = high.len();
751 if cols == 0 {
752 return Err(GopalakrishnanRangeIndexError::EmptyInputData);
753 }
754 if low.len() != cols {
755 return Err(GopalakrishnanRangeIndexError::InconsistentSliceLengths {
756 high_len: high.len(),
757 low_len: low.len(),
758 });
759 }
760
761 let first = first_valid_high_low(high, low);
762 if first >= cols {
763 return Err(GopalakrishnanRangeIndexError::AllValuesNaN);
764 }
765
766 let valid = count_valid_high_low(high, low);
767 let max_length = combos
768 .iter()
769 .map(|combo| combo.length.unwrap_or(5))
770 .max()
771 .unwrap_or(0);
772 if valid < max_length {
773 return Err(GopalakrishnanRangeIndexError::NotEnoughValidData {
774 needed: max_length,
775 valid,
776 });
777 }
778
779 let mut buf_mu = make_uninit_matrix(rows, cols);
780 let warmups: Vec<usize> = combos
781 .iter()
782 .map(|combo| first.saturating_add(combo.length.unwrap_or(5).saturating_sub(1)))
783 .collect();
784 init_matrix_prefixes(&mut buf_mu, cols, &warmups);
785
786 let mut guard = ManuallyDrop::new(buf_mu);
787 let out: &mut [f64] =
788 unsafe { std::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
789 let prefix_valid = build_prefix_valid(high, low);
790
791 if parallel {
792 #[cfg(not(target_arch = "wasm32"))]
793 out.par_chunks_mut(cols)
794 .enumerate()
795 .for_each(|(row, out_row)| {
796 let length = combos[row].length.unwrap_or(5);
797 gapo_row_from_slices(high, low, &prefix_valid, length, first, out_row);
798 });
799
800 #[cfg(target_arch = "wasm32")]
801 for (row, out_row) in out.chunks_mut(cols).enumerate() {
802 let length = combos[row].length.unwrap_or(5);
803 gapo_row_from_slices(high, low, &prefix_valid, length, first, out_row);
804 }
805 } else {
806 for (row, out_row) in out.chunks_mut(cols).enumerate() {
807 let length = combos[row].length.unwrap_or(5);
808 gapo_row_from_slices(high, low, &prefix_valid, length, first, out_row);
809 }
810 }
811
812 let values = unsafe {
813 Vec::from_raw_parts(
814 guard.as_mut_ptr() as *mut f64,
815 guard.len(),
816 guard.capacity(),
817 )
818 };
819
820 Ok(GopalakrishnanRangeIndexBatchOutput {
821 values,
822 combos,
823 rows,
824 cols,
825 })
826}
827
828#[inline(always)]
829pub fn gopalakrishnan_range_index_batch_inner_into(
830 high: &[f64],
831 low: &[f64],
832 sweep: &GopalakrishnanRangeIndexBatchRange,
833 _kernel: Kernel,
834 parallel: bool,
835 out: &mut [f64],
836) -> Result<Vec<GopalakrishnanRangeIndexParams>, GopalakrishnanRangeIndexError> {
837 let combos = expand_grid_gopalakrishnan_range_index(sweep)?;
838 let rows = combos.len();
839 let cols = high.len();
840 if cols == 0 {
841 return Err(GopalakrishnanRangeIndexError::EmptyInputData);
842 }
843 if low.len() != cols {
844 return Err(GopalakrishnanRangeIndexError::InconsistentSliceLengths {
845 high_len: high.len(),
846 low_len: low.len(),
847 });
848 }
849 let total = rows.checked_mul(cols).ok_or_else(|| {
850 GopalakrishnanRangeIndexError::OutputLengthMismatch {
851 expected: usize::MAX,
852 got: out.len(),
853 }
854 })?;
855 if out.len() != total {
856 return Err(GopalakrishnanRangeIndexError::OutputLengthMismatch {
857 expected: total,
858 got: out.len(),
859 });
860 }
861
862 let first = first_valid_high_low(high, low);
863 if first >= cols {
864 return Err(GopalakrishnanRangeIndexError::AllValuesNaN);
865 }
866
867 let valid = count_valid_high_low(high, low);
868 let max_length = combos
869 .iter()
870 .map(|combo| combo.length.unwrap_or(5))
871 .max()
872 .unwrap_or(0);
873 if valid < max_length {
874 return Err(GopalakrishnanRangeIndexError::NotEnoughValidData {
875 needed: max_length,
876 valid,
877 });
878 }
879
880 let prefix_valid = build_prefix_valid(high, low);
881
882 if parallel {
883 #[cfg(not(target_arch = "wasm32"))]
884 out.par_chunks_mut(cols)
885 .enumerate()
886 .for_each(|(row, out_row)| {
887 let length = combos[row].length.unwrap_or(5);
888 gapo_row_from_slices(high, low, &prefix_valid, length, first, out_row);
889 });
890
891 #[cfg(target_arch = "wasm32")]
892 for (row, out_row) in out.chunks_mut(cols).enumerate() {
893 let length = combos[row].length.unwrap_or(5);
894 gapo_row_from_slices(high, low, &prefix_valid, length, first, out_row);
895 }
896 } else {
897 for (row, out_row) in out.chunks_mut(cols).enumerate() {
898 let length = combos[row].length.unwrap_or(5);
899 gapo_row_from_slices(high, low, &prefix_valid, length, first, out_row);
900 }
901 }
902
903 Ok(combos)
904}
905
906#[cfg(feature = "python")]
907#[pyfunction(name = "gopalakrishnan_range_index")]
908#[pyo3(signature = (high, low, length=5, kernel=None))]
909pub fn gopalakrishnan_range_index_py<'py>(
910 py: Python<'py>,
911 high: PyReadonlyArray1<'py, f64>,
912 low: PyReadonlyArray1<'py, f64>,
913 length: usize,
914 kernel: Option<&str>,
915) -> PyResult<Bound<'py, PyArray1<f64>>> {
916 let high = high.as_slice()?;
917 let low = low.as_slice()?;
918 if high.len() != low.len() {
919 return Err(PyValueError::new_err("High/low slice length mismatch"));
920 }
921
922 let kernel = validate_kernel(kernel, false)?;
923 let input = GopalakrishnanRangeIndexInput::from_slices(
924 high,
925 low,
926 GopalakrishnanRangeIndexParams {
927 length: Some(length),
928 },
929 );
930 let output = py
931 .allow_threads(|| gopalakrishnan_range_index_with_kernel(&input, kernel))
932 .map_err(|e| PyValueError::new_err(e.to_string()))?;
933 Ok(output.values.into_pyarray(py))
934}
935
936#[cfg(feature = "python")]
937#[pyclass(name = "GopalakrishnanRangeIndexStream")]
938pub struct GopalakrishnanRangeIndexStreamPy {
939 stream: GopalakrishnanRangeIndexStream,
940}
941
942#[cfg(feature = "python")]
943#[pymethods]
944impl GopalakrishnanRangeIndexStreamPy {
945 #[new]
946 fn new(length: usize) -> PyResult<Self> {
947 let stream = GopalakrishnanRangeIndexStream::try_new(GopalakrishnanRangeIndexParams {
948 length: Some(length),
949 })
950 .map_err(|e| PyValueError::new_err(e.to_string()))?;
951 Ok(Self { stream })
952 }
953
954 fn update(&mut self, high: f64, low: f64) -> Option<f64> {
955 self.stream.update(high, low)
956 }
957}
958
959#[cfg(feature = "python")]
960#[pyfunction(name = "gopalakrishnan_range_index_batch")]
961#[pyo3(signature = (high, low, length_range, kernel=None))]
962pub fn gopalakrishnan_range_index_batch_py<'py>(
963 py: Python<'py>,
964 high: PyReadonlyArray1<'py, f64>,
965 low: PyReadonlyArray1<'py, f64>,
966 length_range: (usize, usize, usize),
967 kernel: Option<&str>,
968) -> PyResult<Bound<'py, PyDict>> {
969 let high = high.as_slice()?;
970 let low = low.as_slice()?;
971 if high.len() != low.len() {
972 return Err(PyValueError::new_err("High/low slice length mismatch"));
973 }
974
975 let kernel = validate_kernel(kernel, true)?;
976 let sweep = GopalakrishnanRangeIndexBatchRange {
977 length: length_range,
978 };
979 let combos = expand_grid_gopalakrishnan_range_index(&sweep)
980 .map_err(|e| PyValueError::new_err(e.to_string()))?;
981 let rows = combos.len();
982 let cols = high.len();
983 let total = rows
984 .checked_mul(cols)
985 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
986
987 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
988 let slice_out = unsafe { out_arr.as_slice_mut()? };
989
990 let combos = py
991 .allow_threads(|| {
992 let batch = match kernel {
993 Kernel::Auto => detect_best_batch_kernel(),
994 other => other,
995 };
996 gopalakrishnan_range_index_batch_inner_into(
997 high,
998 low,
999 &sweep,
1000 batch.to_non_batch(),
1001 true,
1002 slice_out,
1003 )
1004 })
1005 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1006
1007 let dict = PyDict::new(py);
1008 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1009 dict.set_item(
1010 "lengths",
1011 combos
1012 .iter()
1013 .map(|combo| combo.length.unwrap_or(5) as u64)
1014 .collect::<Vec<_>>()
1015 .into_pyarray(py),
1016 )?;
1017 dict.set_item("rows", rows)?;
1018 dict.set_item("cols", cols)?;
1019 Ok(dict)
1020}
1021
1022#[cfg(feature = "python")]
1023pub fn register_gopalakrishnan_range_index_module(
1024 module: &Bound<'_, pyo3::types::PyModule>,
1025) -> PyResult<()> {
1026 module.add_function(wrap_pyfunction!(gopalakrishnan_range_index_py, module)?)?;
1027 module.add_function(wrap_pyfunction!(
1028 gopalakrishnan_range_index_batch_py,
1029 module
1030 )?)?;
1031 module.add_class::<GopalakrishnanRangeIndexStreamPy>()?;
1032 Ok(())
1033}
1034
1035#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1036#[wasm_bindgen(js_name = "gopalakrishnan_range_index_js")]
1037pub fn gopalakrishnan_range_index_js(
1038 high: &[f64],
1039 low: &[f64],
1040 length: usize,
1041) -> Result<Vec<f64>, JsValue> {
1042 let input = GopalakrishnanRangeIndexInput::from_slices(
1043 high,
1044 low,
1045 GopalakrishnanRangeIndexParams {
1046 length: Some(length),
1047 },
1048 );
1049 let mut output = vec![0.0; high.len()];
1050 gopalakrishnan_range_index_into_slice(&mut output, &input, Kernel::Auto)
1051 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1052 Ok(output)
1053}
1054
1055#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1056#[wasm_bindgen]
1057pub fn gopalakrishnan_range_index_alloc(len: usize) -> *mut f64 {
1058 let mut vec = Vec::<f64>::with_capacity(len);
1059 let ptr = vec.as_mut_ptr();
1060 std::mem::forget(vec);
1061 ptr
1062}
1063
1064#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1065#[wasm_bindgen]
1066pub fn gopalakrishnan_range_index_free(ptr: *mut f64, len: usize) {
1067 if !ptr.is_null() {
1068 unsafe {
1069 let _ = Vec::from_raw_parts(ptr, len, len);
1070 }
1071 }
1072}
1073
1074#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1075#[wasm_bindgen]
1076pub fn gopalakrishnan_range_index_into(
1077 high_ptr: *const f64,
1078 low_ptr: *const f64,
1079 out_ptr: *mut f64,
1080 len: usize,
1081 length: usize,
1082) -> Result<(), JsValue> {
1083 if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
1084 return Err(JsValue::from_str("Null pointer provided"));
1085 }
1086
1087 unsafe {
1088 let high = std::slice::from_raw_parts(high_ptr, len);
1089 let low = std::slice::from_raw_parts(low_ptr, len);
1090 let input = GopalakrishnanRangeIndexInput::from_slices(
1091 high,
1092 low,
1093 GopalakrishnanRangeIndexParams {
1094 length: Some(length),
1095 },
1096 );
1097 if high_ptr == out_ptr || low_ptr == out_ptr {
1098 let mut tmp = vec![0.0; len];
1099 gopalakrishnan_range_index_into_slice(&mut tmp, &input, Kernel::Auto)
1100 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1101 std::slice::from_raw_parts_mut(out_ptr, len).copy_from_slice(&tmp);
1102 } else {
1103 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1104 gopalakrishnan_range_index_into_slice(out, &input, Kernel::Auto)
1105 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1106 }
1107 }
1108
1109 Ok(())
1110}
1111
1112#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1113#[derive(Serialize, Deserialize)]
1114pub struct GopalakrishnanRangeIndexBatchConfig {
1115 pub length_range: (usize, usize, usize),
1116}
1117
1118#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1119#[derive(Serialize, Deserialize)]
1120pub struct GopalakrishnanRangeIndexBatchJsOutput {
1121 pub values: Vec<f64>,
1122 pub combos: Vec<GopalakrishnanRangeIndexParams>,
1123 pub lengths: Vec<usize>,
1124 pub rows: usize,
1125 pub cols: usize,
1126}
1127
1128#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1129#[wasm_bindgen(js_name = "gopalakrishnan_range_index_batch_js")]
1130pub fn gopalakrishnan_range_index_batch_js(
1131 high: &[f64],
1132 low: &[f64],
1133 config: JsValue,
1134) -> Result<JsValue, JsValue> {
1135 let config: GopalakrishnanRangeIndexBatchConfig = serde_wasm_bindgen::from_value(config)
1136 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1137 let sweep = GopalakrishnanRangeIndexBatchRange {
1138 length: config.length_range,
1139 };
1140 let output =
1141 gopalakrishnan_range_index_batch_inner(high, low, &sweep, detect_best_kernel(), false)
1142 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1143
1144 serde_wasm_bindgen::to_value(&GopalakrishnanRangeIndexBatchJsOutput {
1145 lengths: output
1146 .combos
1147 .iter()
1148 .map(|combo| combo.length.unwrap_or(5))
1149 .collect(),
1150 values: output.values,
1151 combos: output.combos,
1152 rows: output.rows,
1153 cols: output.cols,
1154 })
1155 .map_err(|e| JsValue::from_str(&e.to_string()))
1156}
1157
1158#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1159#[wasm_bindgen]
1160pub fn gopalakrishnan_range_index_batch_into(
1161 high_ptr: *const f64,
1162 low_ptr: *const f64,
1163 out_ptr: *mut f64,
1164 len: usize,
1165 length_start: usize,
1166 length_end: usize,
1167 length_step: usize,
1168) -> Result<usize, JsValue> {
1169 if high_ptr.is_null() || low_ptr.is_null() || out_ptr.is_null() {
1170 return Err(JsValue::from_str("Null pointer provided"));
1171 }
1172
1173 let sweep = GopalakrishnanRangeIndexBatchRange {
1174 length: (length_start, length_end, length_step),
1175 };
1176 let combos = expand_grid_gopalakrishnan_range_index(&sweep)
1177 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1178 let rows = combos.len();
1179
1180 unsafe {
1181 let high = std::slice::from_raw_parts(high_ptr, len);
1182 let low = std::slice::from_raw_parts(low_ptr, len);
1183 let total = rows
1184 .checked_mul(len)
1185 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1186 let out = std::slice::from_raw_parts_mut(out_ptr, total);
1187 gopalakrishnan_range_index_batch_inner_into(
1188 high,
1189 low,
1190 &sweep,
1191 detect_best_kernel(),
1192 false,
1193 out,
1194 )
1195 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1196 }
1197
1198 Ok(rows)
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use super::*;
1204 use crate::utilities::data_loader::read_candles_from_csv;
1205 use std::error::Error;
1206
1207 fn load_high_low() -> Result<(Vec<f64>, Vec<f64>), Box<dyn Error>> {
1208 let candles = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
1209 Ok((candles.high, candles.low))
1210 }
1211
1212 #[test]
1213 fn gopalakrishnan_range_index_output_contract() -> Result<(), Box<dyn Error>> {
1214 let (high, low) = load_high_low()?;
1215 let input = GopalakrishnanRangeIndexInput::from_slices(
1216 &high,
1217 &low,
1218 GopalakrishnanRangeIndexParams { length: Some(5) },
1219 );
1220 let out = gopalakrishnan_range_index_with_kernel(&input, Kernel::Scalar)?;
1221 assert_eq!(out.values.len(), high.len());
1222 let first_valid = out.values.iter().position(|v| !v.is_nan()).unwrap();
1223 assert!(first_valid >= 4);
1224 assert!(out.values[first_valid..].iter().any(|v| v.is_finite()));
1225 Ok(())
1226 }
1227
1228 #[test]
1229 fn gopalakrishnan_range_index_auto_matches_scalar() -> Result<(), Box<dyn Error>> {
1230 let (high, low) = load_high_low()?;
1231 let input = GopalakrishnanRangeIndexInput::from_slices(
1232 &high,
1233 &low,
1234 GopalakrishnanRangeIndexParams { length: Some(9) },
1235 );
1236 let auto = gopalakrishnan_range_index_with_kernel(&input, Kernel::Auto)?;
1237 let scalar = gopalakrishnan_range_index_with_kernel(&input, Kernel::Scalar)?;
1238 for (a, b) in auto.values.iter().zip(scalar.values.iter()) {
1239 if a.is_nan() && b.is_nan() {
1240 continue;
1241 }
1242 assert!((a - b).abs() <= 1e-12);
1243 }
1244 Ok(())
1245 }
1246
1247 #[test]
1248 fn gopalakrishnan_range_index_into_matches_api() -> Result<(), Box<dyn Error>> {
1249 let (high, low) = load_high_low()?;
1250 let input = GopalakrishnanRangeIndexInput::from_slices(
1251 &high,
1252 &low,
1253 GopalakrishnanRangeIndexParams { length: Some(7) },
1254 );
1255 let api = gopalakrishnan_range_index_with_kernel(&input, Kernel::Auto)?;
1256 let mut out = vec![0.0; high.len()];
1257 gopalakrishnan_range_index_into(&input, &mut out)?;
1258 for (a, b) in api.values.iter().zip(out.iter()) {
1259 if a.is_nan() {
1260 assert!(b.is_nan());
1261 } else {
1262 assert!((a - b).abs() <= 1e-12);
1263 }
1264 }
1265 Ok(())
1266 }
1267
1268 #[test]
1269 fn gopalakrishnan_range_index_stream_matches_batch() -> Result<(), Box<dyn Error>> {
1270 let (high, low) = load_high_low()?;
1271 let params = GopalakrishnanRangeIndexParams { length: Some(8) };
1272 let input = GopalakrishnanRangeIndexInput::from_slices(&high, &low, params.clone());
1273 let batch = gopalakrishnan_range_index_with_kernel(&input, Kernel::Scalar)?;
1274 let mut stream = GopalakrishnanRangeIndexStream::try_new(params)?;
1275 let mut streamed = Vec::with_capacity(high.len());
1276 for i in 0..high.len() {
1277 streamed.push(stream.update(high[i], low[i]).unwrap_or(f64::NAN));
1278 }
1279 for (a, b) in streamed.iter().zip(batch.values.iter()) {
1280 if a.is_nan() && b.is_nan() {
1281 continue;
1282 }
1283 assert!((a - b).abs() <= 1e-12);
1284 }
1285 Ok(())
1286 }
1287
1288 #[test]
1289 fn gopalakrishnan_range_index_batch_matches_single() -> Result<(), Box<dyn Error>> {
1290 let (high, low) = load_high_low()?;
1291 let single = gopalakrishnan_range_index_with_kernel(
1292 &GopalakrishnanRangeIndexInput::from_slices(
1293 &high,
1294 &low,
1295 GopalakrishnanRangeIndexParams { length: Some(10) },
1296 ),
1297 Kernel::Scalar,
1298 )?;
1299 let batch = gopalakrishnan_range_index_batch_with_kernel(
1300 &high,
1301 &low,
1302 &GopalakrishnanRangeIndexBatchRange {
1303 length: (10, 10, 0),
1304 },
1305 Kernel::ScalarBatch,
1306 )?;
1307 assert_eq!(batch.rows, 1);
1308 assert_eq!(batch.cols, high.len());
1309 for (a, b) in batch.values.iter().zip(single.values.iter()) {
1310 if a.is_nan() && b.is_nan() {
1311 continue;
1312 }
1313 assert!((a - b).abs() <= 1e-12);
1314 }
1315 Ok(())
1316 }
1317
1318 #[test]
1319 fn gopalakrishnan_range_index_invalid_window_recovers() -> Result<(), Box<dyn Error>> {
1320 let (mut high, mut low) = load_high_low()?;
1321 high.truncate(80);
1322 low.truncate(80);
1323 high[30] = f64::NAN;
1324 low[30] = f64::NAN;
1325 let out = gopalakrishnan_range_index_with_kernel(
1326 &GopalakrishnanRangeIndexInput::from_slices(
1327 &high,
1328 &low,
1329 GopalakrishnanRangeIndexParams { length: Some(10) },
1330 ),
1331 Kernel::Scalar,
1332 )?;
1333 assert!(out.values[30].is_nan());
1334 assert!(out.values[39].is_nan());
1335 assert!(out.values[40].is_finite());
1336 Ok(())
1337 }
1338
1339 #[test]
1340 fn gopalakrishnan_range_index_rejects_invalid_length() {
1341 let high = [1.0, 2.0, 3.0];
1342 let low = [0.5, 1.5, 2.5];
1343 let err = gopalakrishnan_range_index_with_kernel(
1344 &GopalakrishnanRangeIndexInput::from_slices(
1345 &high,
1346 &low,
1347 GopalakrishnanRangeIndexParams { length: Some(1) },
1348 ),
1349 Kernel::Scalar,
1350 )
1351 .unwrap_err();
1352 assert!(matches!(
1353 err,
1354 GopalakrishnanRangeIndexError::InvalidLength { .. }
1355 ));
1356 }
1357}