Skip to main content

vector_ta/indicators/
gopalakrishnan_range_index.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19    make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23#[cfg(not(target_arch = "wasm32"))]
24use rayon::prelude::*;
25use std::collections::VecDeque;
26use std::mem::ManuallyDrop;
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}