Skip to main content

vector_ta/indicators/
possible_rsi.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::indicators::rsi::{rsi_into_slice, RsiInput, RsiParams};
16use crate::indicators::rsx::{rsx_into_slice, RsxInput, RsxParams};
17use crate::utilities::data_loader::{source_type, Candles};
18use crate::utilities::enums::Kernel;
19use crate::utilities::helpers::{
20    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
21    make_uninit_matrix,
22};
23#[cfg(feature = "python")]
24use crate::utilities::kernel_validation::validate_kernel;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::error::Error;
28use thiserror::Error;
29
30const DEFAULT_SOURCE: &str = "close";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum PossibleRsiMode {
34    Rsx,
35    Regular,
36    Slow,
37    Rapid,
38    Harris,
39    Cutler,
40    EhlersSmoothed,
41}
42
43impl PossibleRsiMode {
44    #[inline(always)]
45    fn from_str(value: &str) -> Result<Self, PossibleRsiError> {
46        if value.eq_ignore_ascii_case("rsx") {
47            return Ok(Self::Rsx);
48        }
49        if value.eq_ignore_ascii_case("regular") {
50            return Ok(Self::Regular);
51        }
52        if value.eq_ignore_ascii_case("slow") {
53            return Ok(Self::Slow);
54        }
55        if value.eq_ignore_ascii_case("rapid") {
56            return Ok(Self::Rapid);
57        }
58        if value.eq_ignore_ascii_case("harris") {
59            return Ok(Self::Harris);
60        }
61        if value.eq_ignore_ascii_case("cutler") || value.eq_ignore_ascii_case("cuttler") {
62            return Ok(Self::Cutler);
63        }
64        if value.eq_ignore_ascii_case("ehlers_smoothed")
65            || value.eq_ignore_ascii_case("ehlers-smoothed")
66            || value.eq_ignore_ascii_case("ehlers smoothed")
67        {
68            return Ok(Self::EhlersSmoothed);
69        }
70        Err(PossibleRsiError::InvalidRsiMode {
71            rsi_mode: value.to_string(),
72        })
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77enum PossibleRsiNormalizationMode {
78    GaussianFisher,
79    Softmax,
80    RegularNorm,
81}
82
83impl PossibleRsiNormalizationMode {
84    #[inline(always)]
85    fn from_str(value: &str) -> Result<Self, PossibleRsiError> {
86        if value.eq_ignore_ascii_case("gaussian_fisher")
87            || value.eq_ignore_ascii_case("gaussian")
88            || value.eq_ignore_ascii_case("gaussian (fisher)")
89            || value.eq_ignore_ascii_case("fisher")
90        {
91            return Ok(Self::GaussianFisher);
92        }
93        if value.eq_ignore_ascii_case("softmax") {
94            return Ok(Self::Softmax);
95        }
96        if value.eq_ignore_ascii_case("regular_norm")
97            || value.eq_ignore_ascii_case("regular norm")
98            || value.eq_ignore_ascii_case("regnorm")
99        {
100            return Ok(Self::RegularNorm);
101        }
102        Err(PossibleRsiError::InvalidNormalizationMode {
103            normalization_mode: value.to_string(),
104        })
105    }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109enum PossibleRsiSignalType {
110    Slope,
111    DynamicMiddleCrossover,
112    LevelsCrossover,
113    ZerolineCrossover,
114}
115
116impl PossibleRsiSignalType {
117    #[inline(always)]
118    fn from_str(value: &str) -> Result<Self, PossibleRsiError> {
119        if value.eq_ignore_ascii_case("slope") {
120            return Ok(Self::Slope);
121        }
122        if value.eq_ignore_ascii_case("dynamic_middle_crossover")
123            || value.eq_ignore_ascii_case("dynamic middle crossover")
124        {
125            return Ok(Self::DynamicMiddleCrossover);
126        }
127        if value.eq_ignore_ascii_case("levels_crossover")
128            || value.eq_ignore_ascii_case("levels crossover")
129        {
130            return Ok(Self::LevelsCrossover);
131        }
132        if value.eq_ignore_ascii_case("zeroline_crossover")
133            || value.eq_ignore_ascii_case("zeroline crossover")
134            || value.eq_ignore_ascii_case("zero_line_crossover")
135        {
136            return Ok(Self::ZerolineCrossover);
137        }
138        Err(PossibleRsiError::InvalidSignalType {
139            signal_type: value.to_string(),
140        })
141    }
142}
143
144#[derive(Debug, Clone)]
145pub enum PossibleRsiData<'a> {
146    Candles {
147        candles: &'a Candles,
148        source: &'a str,
149    },
150    Slice(&'a [f64]),
151}
152
153#[derive(Debug, Clone)]
154pub struct PossibleRsiOutput {
155    pub value: Vec<f64>,
156    pub buy_level: Vec<f64>,
157    pub sell_level: Vec<f64>,
158    pub middle_level: Vec<f64>,
159    pub state: Vec<f64>,
160    pub long_signal: Vec<f64>,
161    pub short_signal: Vec<f64>,
162}
163
164#[derive(Debug, Clone)]
165#[cfg_attr(
166    all(target_arch = "wasm32", feature = "wasm"),
167    derive(Serialize, Deserialize)
168)]
169pub struct PossibleRsiParams {
170    pub period: Option<usize>,
171    pub rsi_mode: Option<String>,
172    pub norm_period: Option<usize>,
173    pub normalization_mode: Option<String>,
174    pub normalization_length: Option<usize>,
175    pub nonlag_period: Option<usize>,
176    pub dynamic_zone_period: Option<usize>,
177    pub buy_probability: Option<f64>,
178    pub sell_probability: Option<f64>,
179    pub signal_type: Option<String>,
180    pub run_highpass: Option<bool>,
181    pub highpass_period: Option<usize>,
182}
183
184impl Default for PossibleRsiParams {
185    fn default() -> Self {
186        Self {
187            period: Some(32),
188            rsi_mode: Some("regular".to_string()),
189            norm_period: Some(100),
190            normalization_mode: Some("gaussian_fisher".to_string()),
191            normalization_length: Some(15),
192            nonlag_period: Some(15),
193            dynamic_zone_period: Some(20),
194            buy_probability: Some(0.2),
195            sell_probability: Some(0.2),
196            signal_type: Some("zeroline_crossover".to_string()),
197            run_highpass: Some(false),
198            highpass_period: Some(15),
199        }
200    }
201}
202
203#[derive(Debug, Clone)]
204pub struct PossibleRsiInput<'a> {
205    pub data: PossibleRsiData<'a>,
206    pub params: PossibleRsiParams,
207}
208
209impl<'a> PossibleRsiInput<'a> {
210    #[inline]
211    pub fn from_candles(candles: &'a Candles, source: &'a str, params: PossibleRsiParams) -> Self {
212        Self {
213            data: PossibleRsiData::Candles { candles, source },
214            params,
215        }
216    }
217
218    #[inline]
219    pub fn from_slice(data: &'a [f64], params: PossibleRsiParams) -> Self {
220        Self {
221            data: PossibleRsiData::Slice(data),
222            params,
223        }
224    }
225
226    #[inline]
227    pub fn with_default_candles(candles: &'a Candles) -> Self {
228        Self::from_candles(candles, DEFAULT_SOURCE, PossibleRsiParams::default())
229    }
230}
231
232#[derive(Copy, Clone, Debug)]
233pub struct PossibleRsiBuilder {
234    period: Option<usize>,
235    rsi_mode: Option<&'static str>,
236    norm_period: Option<usize>,
237    normalization_mode: Option<&'static str>,
238    normalization_length: Option<usize>,
239    nonlag_period: Option<usize>,
240    dynamic_zone_period: Option<usize>,
241    buy_probability: Option<f64>,
242    sell_probability: Option<f64>,
243    signal_type: Option<&'static str>,
244    run_highpass: Option<bool>,
245    highpass_period: Option<usize>,
246    kernel: Kernel,
247}
248
249impl Default for PossibleRsiBuilder {
250    fn default() -> Self {
251        Self {
252            period: None,
253            rsi_mode: None,
254            norm_period: None,
255            normalization_mode: None,
256            normalization_length: None,
257            nonlag_period: None,
258            dynamic_zone_period: None,
259            buy_probability: None,
260            sell_probability: None,
261            signal_type: None,
262            run_highpass: None,
263            highpass_period: None,
264            kernel: Kernel::Auto,
265        }
266    }
267}
268
269impl PossibleRsiBuilder {
270    #[inline(always)]
271    pub fn new() -> Self {
272        Self::default()
273    }
274
275    #[inline(always)]
276    pub fn period(mut self, value: usize) -> Self {
277        self.period = Some(value);
278        self
279    }
280
281    #[inline(always)]
282    pub fn rsi_mode(mut self, value: &'static str) -> Self {
283        self.rsi_mode = Some(value);
284        self
285    }
286
287    #[inline(always)]
288    pub fn norm_period(mut self, value: usize) -> Self {
289        self.norm_period = Some(value);
290        self
291    }
292
293    #[inline(always)]
294    pub fn normalization_mode(mut self, value: &'static str) -> Self {
295        self.normalization_mode = Some(value);
296        self
297    }
298
299    #[inline(always)]
300    pub fn normalization_length(mut self, value: usize) -> Self {
301        self.normalization_length = Some(value);
302        self
303    }
304
305    #[inline(always)]
306    pub fn nonlag_period(mut self, value: usize) -> Self {
307        self.nonlag_period = Some(value);
308        self
309    }
310
311    #[inline(always)]
312    pub fn dynamic_zone_period(mut self, value: usize) -> Self {
313        self.dynamic_zone_period = Some(value);
314        self
315    }
316
317    #[inline(always)]
318    pub fn buy_probability(mut self, value: f64) -> Self {
319        self.buy_probability = Some(value);
320        self
321    }
322
323    #[inline(always)]
324    pub fn sell_probability(mut self, value: f64) -> Self {
325        self.sell_probability = Some(value);
326        self
327    }
328
329    #[inline(always)]
330    pub fn signal_type(mut self, value: &'static str) -> Self {
331        self.signal_type = Some(value);
332        self
333    }
334
335    #[inline(always)]
336    pub fn run_highpass(mut self, value: bool) -> Self {
337        self.run_highpass = Some(value);
338        self
339    }
340
341    #[inline(always)]
342    pub fn highpass_period(mut self, value: usize) -> Self {
343        self.highpass_period = Some(value);
344        self
345    }
346
347    #[inline(always)]
348    pub fn kernel(mut self, value: Kernel) -> Self {
349        self.kernel = value;
350        self
351    }
352
353    #[inline(always)]
354    fn build_params(self) -> PossibleRsiParams {
355        PossibleRsiParams {
356            period: self.period,
357            rsi_mode: self.rsi_mode.map(str::to_string),
358            norm_period: self.norm_period,
359            normalization_mode: self.normalization_mode.map(str::to_string),
360            normalization_length: self.normalization_length,
361            nonlag_period: self.nonlag_period,
362            dynamic_zone_period: self.dynamic_zone_period,
363            buy_probability: self.buy_probability,
364            sell_probability: self.sell_probability,
365            signal_type: self.signal_type.map(str::to_string),
366            run_highpass: self.run_highpass,
367            highpass_period: self.highpass_period,
368        }
369    }
370
371    #[inline(always)]
372    pub fn apply(self, candles: &Candles) -> Result<PossibleRsiOutput, PossibleRsiError> {
373        self.apply_candles(candles, DEFAULT_SOURCE)
374    }
375
376    #[inline(always)]
377    pub fn apply_candles(
378        self,
379        candles: &Candles,
380        source: &str,
381    ) -> Result<PossibleRsiOutput, PossibleRsiError> {
382        possible_rsi_with_kernel(
383            &PossibleRsiInput::from_candles(candles, source, self.build_params()),
384            self.kernel,
385        )
386    }
387
388    #[inline(always)]
389    pub fn apply_slice(self, data: &[f64]) -> Result<PossibleRsiOutput, PossibleRsiError> {
390        possible_rsi_with_kernel(
391            &PossibleRsiInput::from_slice(data, self.build_params()),
392            self.kernel,
393        )
394    }
395
396    #[inline(always)]
397    pub fn into_stream(self) -> Result<PossibleRsiStream, PossibleRsiError> {
398        PossibleRsiStream::try_new(self.build_params())
399    }
400}
401
402#[derive(Debug, Error)]
403pub enum PossibleRsiError {
404    #[error("possible_rsi: Input data slice is empty.")]
405    EmptyInputData,
406    #[error("possible_rsi: All values are NaN.")]
407    AllValuesNaN,
408    #[error("possible_rsi: Invalid period: {period}")]
409    InvalidPeriod { period: usize },
410    #[error("possible_rsi: Invalid norm_period: {norm_period}")]
411    InvalidNormPeriod { norm_period: usize },
412    #[error("possible_rsi: Invalid normalization_length: {normalization_length}")]
413    InvalidNormalizationLength { normalization_length: usize },
414    #[error("possible_rsi: Invalid nonlag_period: {nonlag_period}")]
415    InvalidNonlagPeriod { nonlag_period: usize },
416    #[error("possible_rsi: Invalid dynamic_zone_period: {dynamic_zone_period}")]
417    InvalidDynamicZonePeriod { dynamic_zone_period: usize },
418    #[error("possible_rsi: Invalid highpass_period: {highpass_period}")]
419    InvalidHighpassPeriod { highpass_period: usize },
420    #[error("possible_rsi: Invalid buy_probability: {buy_probability}")]
421    InvalidBuyProbability { buy_probability: f64 },
422    #[error("possible_rsi: Invalid sell_probability: {sell_probability}")]
423    InvalidSellProbability { sell_probability: f64 },
424    #[error("possible_rsi: Invalid RSI mode: {rsi_mode}")]
425    InvalidRsiMode { rsi_mode: String },
426    #[error("possible_rsi: Invalid normalization_mode: {normalization_mode}")]
427    InvalidNormalizationMode { normalization_mode: String },
428    #[error("possible_rsi: Invalid signal_type: {signal_type}")]
429    InvalidSignalType { signal_type: String },
430    #[error("possible_rsi: Not enough valid data: needed = {needed}, valid = {valid}")]
431    NotEnoughValidData { needed: usize, valid: usize },
432    #[error("possible_rsi: Output length mismatch: expected = {expected}, got = {got}")]
433    OutputLengthMismatch { expected: usize, got: usize },
434    #[error("possible_rsi: Invalid range: {field} start={start} end={end} step={step}")]
435    InvalidRange {
436        field: &'static str,
437        start: String,
438        end: String,
439        step: String,
440    },
441    #[error("possible_rsi: Invalid kernel for batch: {0:?}")]
442    InvalidKernelForBatch(Kernel),
443    #[error("possible_rsi: Output length mismatch: dst = {dst_len}, expected = {expected_len}")]
444    MismatchedOutputLen { dst_len: usize, expected_len: usize },
445    #[error("possible_rsi: Invalid input: {msg}")]
446    InvalidInput { msg: String },
447    #[error("possible_rsi: RSI helper failed: {details}")]
448    RsiFailure { details: String },
449    #[error("possible_rsi: RSX helper failed: {details}")]
450    RsxFailure { details: String },
451}
452
453#[derive(Debug, Clone, Copy)]
454struct PossibleRsiResolved {
455    period: usize,
456    rsi_mode: PossibleRsiMode,
457    norm_period: usize,
458    normalization_mode: PossibleRsiNormalizationMode,
459    normalization_length: usize,
460    nonlag_period: usize,
461    dynamic_zone_period: usize,
462    buy_probability: f64,
463    sell_probability: f64,
464    signal_type: PossibleRsiSignalType,
465    run_highpass: bool,
466    highpass_period: usize,
467}
468
469#[derive(Debug, Clone, Copy)]
470pub struct PossibleRsiPoint {
471    pub value: f64,
472    pub buy_level: f64,
473    pub sell_level: f64,
474    pub middle_level: f64,
475    pub state: f64,
476    pub long_signal: f64,
477    pub short_signal: f64,
478}
479
480#[derive(Debug, Clone)]
481pub struct PossibleRsiStream {
482    params: PossibleRsiParams,
483    history: Vec<f64>,
484}
485
486impl PossibleRsiStream {
487    #[inline(always)]
488    pub fn try_new(params: PossibleRsiParams) -> Result<Self, PossibleRsiError> {
489        let _ = resolve_params(&params)?;
490        Ok(Self {
491            params,
492            history: Vec::new(),
493        })
494    }
495
496    #[inline(always)]
497    pub fn reset(&mut self) {
498        self.history.clear();
499    }
500
501    #[inline(always)]
502    pub fn update(&mut self, value: f64) -> Option<PossibleRsiPoint> {
503        if !value.is_finite() {
504            self.reset();
505            return None;
506        }
507        self.history.push(value);
508        let out = possible_rsi(&PossibleRsiInput::from_slice(
509            &self.history,
510            self.params.clone(),
511        ))
512        .ok()?;
513        let i = self.history.len().saturating_sub(1);
514        let point = PossibleRsiPoint {
515            value: out.value[i],
516            buy_level: out.buy_level[i],
517            sell_level: out.sell_level[i],
518            middle_level: out.middle_level[i],
519            state: out.state[i],
520            long_signal: out.long_signal[i],
521            short_signal: out.short_signal[i],
522        };
523        if point.value.is_finite()
524            && point.buy_level.is_finite()
525            && point.sell_level.is_finite()
526            && point.middle_level.is_finite()
527            && point.state.is_finite()
528        {
529            Some(point)
530        } else {
531            None
532        }
533    }
534
535    #[inline(always)]
536    pub fn get_warmup_period(&self) -> usize {
537        resolve_params(&self.params)
538            .map(estimated_warmup)
539            .unwrap_or(0)
540    }
541}
542
543#[inline(always)]
544fn input_slice<'a>(input: &'a PossibleRsiInput<'a>) -> &'a [f64] {
545    match &input.data {
546        PossibleRsiData::Candles { candles, source } => source_type(candles, source),
547        PossibleRsiData::Slice(values) => values,
548    }
549}
550
551#[inline(always)]
552fn longest_valid_run(data: &[f64]) -> usize {
553    let mut best = 0usize;
554    let mut current = 0usize;
555    for &value in data {
556        if value.is_finite() {
557            current += 1;
558            if current > best {
559                best = current;
560            }
561        } else {
562            current = 0;
563        }
564    }
565    best
566}
567
568#[inline(always)]
569fn nonlag_kernel_len(period: usize) -> usize {
570    period.saturating_mul(5).saturating_sub(1)
571}
572
573#[inline(always)]
574fn estimated_warmup(params: PossibleRsiResolved) -> usize {
575    params
576        .period
577        .saturating_add(params.norm_period.saturating_sub(1))
578        .saturating_add(params.normalization_length.saturating_sub(1))
579        .saturating_add(nonlag_kernel_len(params.nonlag_period).saturating_sub(1))
580        .saturating_add(params.dynamic_zone_period.saturating_sub(1))
581}
582
583#[inline(always)]
584fn resolve_params(params: &PossibleRsiParams) -> Result<PossibleRsiResolved, PossibleRsiError> {
585    let period = params.period.unwrap_or(32);
586    if period == 0 {
587        return Err(PossibleRsiError::InvalidPeriod { period });
588    }
589    let norm_period = params.norm_period.unwrap_or(100);
590    if norm_period == 0 {
591        return Err(PossibleRsiError::InvalidNormPeriod { norm_period });
592    }
593    let normalization_length = params.normalization_length.unwrap_or(15);
594    if normalization_length == 0 {
595        return Err(PossibleRsiError::InvalidNormalizationLength {
596            normalization_length,
597        });
598    }
599    let nonlag_period = params.nonlag_period.unwrap_or(15);
600    if nonlag_period == 0 {
601        return Err(PossibleRsiError::InvalidNonlagPeriod { nonlag_period });
602    }
603    let dynamic_zone_period = params.dynamic_zone_period.unwrap_or(20);
604    if dynamic_zone_period == 0 {
605        return Err(PossibleRsiError::InvalidDynamicZonePeriod {
606            dynamic_zone_period,
607        });
608    }
609    let buy_probability = params.buy_probability.unwrap_or(0.2);
610    if !buy_probability.is_finite() || !(0.0..=0.5).contains(&buy_probability) {
611        return Err(PossibleRsiError::InvalidBuyProbability { buy_probability });
612    }
613    let sell_probability = params.sell_probability.unwrap_or(0.2);
614    if !sell_probability.is_finite() || !(0.0..=0.5).contains(&sell_probability) {
615        return Err(PossibleRsiError::InvalidSellProbability { sell_probability });
616    }
617    let highpass_period = params.highpass_period.unwrap_or(15);
618    if highpass_period == 0 {
619        return Err(PossibleRsiError::InvalidHighpassPeriod { highpass_period });
620    }
621    Ok(PossibleRsiResolved {
622        period,
623        rsi_mode: PossibleRsiMode::from_str(params.rsi_mode.as_deref().unwrap_or("regular"))?,
624        norm_period,
625        normalization_mode: PossibleRsiNormalizationMode::from_str(
626            params
627                .normalization_mode
628                .as_deref()
629                .unwrap_or("gaussian_fisher"),
630        )?,
631        normalization_length,
632        nonlag_period,
633        dynamic_zone_period,
634        buy_probability,
635        sell_probability,
636        signal_type: PossibleRsiSignalType::from_str(
637            params
638                .signal_type
639                .as_deref()
640                .unwrap_or("zeroline_crossover"),
641        )?,
642        run_highpass: params.run_highpass.unwrap_or(false),
643        highpass_period,
644    })
645}
646#[inline(always)]
647fn validate_common(data: &[f64], params: PossibleRsiResolved) -> Result<(), PossibleRsiError> {
648    if data.is_empty() {
649        return Err(PossibleRsiError::EmptyInputData);
650    }
651    let valid = longest_valid_run(data);
652    if valid == 0 {
653        return Err(PossibleRsiError::AllValuesNaN);
654    }
655    let needed = estimated_warmup(params).saturating_add(1);
656    if valid < needed {
657        return Err(PossibleRsiError::NotEnoughValidData { needed, valid });
658    }
659    Ok(())
660}
661
662#[inline(always)]
663fn for_each_finite_segment<F>(data: &[f64], mut f: F)
664where
665    F: FnMut(usize, usize),
666{
667    let mut start = 0usize;
668    while start < data.len() {
669        while start < data.len() && !data[start].is_finite() {
670            start += 1;
671        }
672        if start >= data.len() {
673            break;
674        }
675        let mut end = start + 1;
676        while end < data.len() && data[end].is_finite() {
677            end += 1;
678        }
679        f(start, end);
680        start = end;
681    }
682}
683
684#[inline(always)]
685fn highpass_series(data: &[f64], period: usize) -> Vec<f64> {
686    let mut out = vec![f64::NAN; data.len()];
687    let a1 = (-1.414 * std::f64::consts::PI / period as f64).exp();
688    let b1 = 2.0 * a1 * (1.414 * std::f64::consts::PI / period as f64).cos();
689    let c2 = b1;
690    let c3 = -a1 * a1;
691    let c1 = (1.0 + c2 - c3) / 4.0;
692    for_each_finite_segment(data, |start, end| {
693        let mut hp1 = 0.0;
694        let mut hp2 = 0.0;
695        for i in start..end {
696            if i - start < 4 {
697                out[i] = 0.0;
698                hp2 = hp1;
699                hp1 = 0.0;
700                continue;
701            }
702            let hp = c1 * (data[i] - 2.0 * data[i - 1] + data[i - 2]) + c2 * hp1 + c3 * hp2;
703            out[i] = hp;
704            hp2 = hp1;
705            hp1 = hp;
706        }
707    });
708    out
709}
710
711#[inline(always)]
712fn cutler_rsi_series(data: &[f64], period: usize) -> Vec<f64> {
713    let mut out = vec![f64::NAN; data.len()];
714    for_each_finite_segment(data, |start, end| {
715        if end - start <= period {
716            return;
717        }
718        let mut gain = 0.0;
719        let mut loss = 0.0;
720        for i in (start + 1)..=(start + period) {
721            let diff = data[i] - data[i - 1];
722            if diff > 0.0 {
723                gain += diff;
724            } else {
725                loss += -diff;
726            }
727        }
728        out[start + period] = if gain + loss == 0.0 {
729            50.0
730        } else {
731            100.0 * gain / (gain + loss)
732        };
733        for i in (start + period + 1)..end {
734            let old_diff = data[i - period] - data[i - period - 1];
735            if old_diff > 0.0 {
736                gain -= old_diff;
737            } else {
738                loss -= -old_diff;
739            }
740            let new_diff = data[i] - data[i - 1];
741            if new_diff > 0.0 {
742                gain += new_diff;
743            } else {
744                loss += -new_diff;
745            }
746            out[i] = if gain + loss == 0.0 {
747                50.0
748            } else {
749                100.0 * gain / (gain + loss)
750            };
751        }
752    });
753    out
754}
755
756#[inline(always)]
757fn harris_rsi_series(data: &[f64], period: usize) -> Vec<f64> {
758    let mut out = vec![f64::NAN; data.len()];
759    for_each_finite_segment(data, |start, end| {
760        if end - start <= period {
761            return;
762        }
763        for i in (start + period)..end {
764            let current = data[i];
765            let mut up = 0.0;
766            let mut down = 0.0;
767            for j in 1..=period {
768                let diff = current - data[i - j];
769                if diff > 0.0 {
770                    up += diff;
771                } else {
772                    down += -diff;
773                }
774            }
775            out[i] = if up + down == 0.0 {
776                50.0
777            } else {
778                100.0 * up / (up + down)
779            };
780        }
781    });
782    out
783}
784
785#[inline(always)]
786fn ehlers_smoothed_rsi_series(data: &[f64], period: usize) -> Vec<f64> {
787    let mut smooth = vec![f64::NAN; data.len()];
788    for_each_finite_segment(data, |start, end| {
789        if end - start < 4 {
790            return;
791        }
792        for i in (start + 3)..end {
793            smooth[i] = (data[i] + 2.0 * data[i - 1] + 2.0 * data[i - 2] + data[i - 3]) / 6.0;
794        }
795    });
796    cutler_rsi_series(&smooth, period)
797}
798
799#[inline(always)]
800fn ema_valid_series(data: &[f64], period: usize) -> Vec<f64> {
801    let mut out = vec![f64::NAN; data.len()];
802    let alpha = 2.0 / (period as f64 + 1.0);
803    let mut state = None;
804    for (i, &value) in data.iter().enumerate() {
805        if !value.is_finite() {
806            state = None;
807            continue;
808        }
809        let next = match state {
810            Some(prev) => prev + alpha * (value - prev),
811            None => value,
812        };
813        out[i] = next;
814        state = Some(next);
815    }
816    out
817}
818
819#[inline(always)]
820fn compute_rsi_series(
821    data: &[f64],
822    mode: PossibleRsiMode,
823    period: usize,
824) -> Result<Vec<f64>, PossibleRsiError> {
825    match mode {
826        PossibleRsiMode::Regular => {
827            let mut out = vec![f64::NAN; data.len()];
828            let input = RsiInput::from_slice(
829                data,
830                RsiParams {
831                    period: Some(period),
832                },
833            );
834            rsi_into_slice(&mut out, &input, Kernel::Auto).map_err(|e| {
835                PossibleRsiError::RsiFailure {
836                    details: e.to_string(),
837                }
838            })?;
839            Ok(out)
840        }
841        PossibleRsiMode::Rsx => {
842            let mut out = vec![f64::NAN; data.len()];
843            let input = RsxInput::from_slice(
844                data,
845                RsxParams {
846                    period: Some(period),
847                },
848            );
849            rsx_into_slice(&mut out, &input, Kernel::Auto).map_err(|e| {
850                PossibleRsiError::RsxFailure {
851                    details: e.to_string(),
852                }
853            })?;
854            Ok(out)
855        }
856        PossibleRsiMode::Cutler | PossibleRsiMode::Rapid => Ok(cutler_rsi_series(data, period)),
857        PossibleRsiMode::Slow => Ok(ema_valid_series(
858            &compute_rsi_series(data, PossibleRsiMode::Regular, period)?,
859            period,
860        )),
861        PossibleRsiMode::Harris => Ok(harris_rsi_series(data, period)),
862        PossibleRsiMode::EhlersSmoothed => Ok(ehlers_smoothed_rsi_series(data, period)),
863    }
864}
865
866#[inline(always)]
867fn rolling_min_max(data: &[f64], period: usize) -> (Vec<f64>, Vec<f64>) {
868    let mut mins = vec![f64::NAN; data.len()];
869    let mut maxs = vec![f64::NAN; data.len()];
870    for_each_finite_segment(data, |start, end| {
871        if end - start < period {
872            return;
873        }
874        let mut min_q: std::collections::VecDeque<usize> =
875            std::collections::VecDeque::with_capacity(period);
876        let mut max_q: std::collections::VecDeque<usize> =
877            std::collections::VecDeque::with_capacity(period);
878        for i in start..end {
879            while let Some(&front) = min_q.front() {
880                if front + period <= i {
881                    min_q.pop_front();
882                } else {
883                    break;
884                }
885            }
886            while let Some(&front) = max_q.front() {
887                if front + period <= i {
888                    max_q.pop_front();
889                } else {
890                    break;
891                }
892            }
893            while let Some(&back) = min_q.back() {
894                if data[back] >= data[i] {
895                    min_q.pop_back();
896                } else {
897                    break;
898                }
899            }
900            while let Some(&back) = max_q.back() {
901                if data[back] <= data[i] {
902                    max_q.pop_back();
903                } else {
904                    break;
905                }
906            }
907            min_q.push_back(i);
908            max_q.push_back(i);
909            if i + 1 >= start + period {
910                mins[i] = data[*min_q.front().unwrap()];
911                maxs[i] = data[*max_q.front().unwrap()];
912            }
913        }
914    });
915    (mins, maxs)
916}
917
918#[inline(always)]
919fn rolling_mean_std(data: &[f64], period: usize) -> (Vec<f64>, Vec<f64>) {
920    let mut means = vec![f64::NAN; data.len()];
921    let mut stds = vec![f64::NAN; data.len()];
922    for_each_finite_segment(data, |start, end| {
923        if end - start < period {
924            return;
925        }
926        let mut sum = 0.0;
927        let mut sumsq = 0.0;
928        for i in start..end {
929            let value = data[i];
930            sum += value;
931            sumsq += value * value;
932            if i >= start + period {
933                let old = data[i - period];
934                sum -= old;
935                sumsq -= old * old;
936            }
937            if i + 1 >= start + period {
938                let mean = sum / period as f64;
939                let mut var = sumsq / period as f64 - mean * mean;
940                if var < 0.0 {
941                    var = 0.0;
942                }
943                means[i] = mean;
944                stds[i] = var.sqrt();
945            }
946        }
947    });
948    (means, stds)
949}
950
951#[inline(always)]
952fn fisher_transform_series(data: &[f64], period: usize) -> Vec<f64> {
953    let (mins, maxs) = rolling_min_max(data, period);
954    let mut out = vec![f64::NAN; data.len()];
955    let mut prev_value = 0.0;
956    let mut prev_fish = 0.0;
957    let mut seeded = false;
958    for i in 0..data.len() {
959        let src = data[i];
960        let low = mins[i];
961        let high = maxs[i];
962        if !src.is_finite()
963            || !low.is_finite()
964            || !high.is_finite()
965            || (high - low).abs() <= f64::EPSILON
966        {
967            seeded = false;
968            prev_value = 0.0;
969            prev_fish = 0.0;
970            continue;
971        }
972        let mut value = 0.66 * ((src - low) / (high - low) - 0.5)
973            + 0.67 * if seeded { prev_value } else { 0.0 };
974        if value > 0.99 {
975            value = 0.999;
976        }
977        if value < -0.99 {
978            value = -0.999;
979        }
980        let fish =
981            0.5 * ((1.0 + value) / (1.0 - value)).ln() + 0.5 * if seeded { prev_fish } else { 0.0 };
982        out[i] = fish;
983        prev_value = value;
984        prev_fish = fish;
985        seeded = true;
986    }
987    out
988}
989
990#[inline(always)]
991fn softmax_series(data: &[f64], period: usize) -> Vec<f64> {
992    let (means, stds) = rolling_mean_std(data, period);
993    let mut out = vec![f64::NAN; data.len()];
994    for i in 0..data.len() {
995        if !data[i].is_finite()
996            || !means[i].is_finite()
997            || !stds[i].is_finite()
998            || stds[i] <= f64::EPSILON
999        {
1000            continue;
1001        }
1002        let z = (data[i] - means[i]) / stds[i];
1003        let exp = (-z).exp();
1004        out[i] = (1.0 - exp) / (1.0 + exp);
1005    }
1006    out
1007}
1008
1009#[inline(always)]
1010fn regular_norm_series(data: &[f64], period: usize) -> Vec<f64> {
1011    let (means, stds) = rolling_mean_std(data, period);
1012    let mut out = vec![f64::NAN; data.len()];
1013    for i in 0..data.len() {
1014        if !data[i].is_finite()
1015            || !means[i].is_finite()
1016            || !stds[i].is_finite()
1017            || stds[i] <= f64::EPSILON
1018        {
1019            continue;
1020        }
1021        out[i] = (data[i] - means[i]) / (stds[i] * 3.0);
1022    }
1023    out
1024}
1025
1026#[inline(always)]
1027fn normalize_min_max(data: &[f64], period: usize) -> Vec<f64> {
1028    let (mins, maxs) = rolling_min_max(data, period);
1029    let mut out = vec![f64::NAN; data.len()];
1030    for i in 0..data.len() {
1031        if !data[i].is_finite()
1032            || !mins[i].is_finite()
1033            || !maxs[i].is_finite()
1034            || (maxs[i] - mins[i]).abs() <= f64::EPSILON
1035        {
1036            continue;
1037        }
1038        out[i] = 100.0 * (data[i] - mins[i]) / (maxs[i] - mins[i]);
1039    }
1040    out
1041}
1042
1043#[inline(always)]
1044fn apply_secondary_normalization(
1045    data: &[f64],
1046    mode: PossibleRsiNormalizationMode,
1047    period: usize,
1048) -> Vec<f64> {
1049    match mode {
1050        PossibleRsiNormalizationMode::GaussianFisher => fisher_transform_series(data, period),
1051        PossibleRsiNormalizationMode::Softmax => softmax_series(data, period),
1052        PossibleRsiNormalizationMode::RegularNorm => regular_norm_series(data, period),
1053    }
1054}
1055
1056#[inline(always)]
1057fn build_nonlag_weights(period: usize) -> (Vec<f64>, f64) {
1058    let cycle = 4.0;
1059    let coeff = 3.0 * std::f64::consts::PI;
1060    let phase = period as f64 - 1.0;
1061    let len = (period as f64 * cycle + phase) as usize;
1062    let mut weights = vec![0.0; len];
1063    let mut weight_sum = 0.0;
1064    for k in 0..len {
1065        let t = if phase > 1.0 && (k as f64) <= phase - 1.0 {
1066            k as f64 / (phase - 1.0)
1067        } else {
1068            1.0 + (k as f64 - phase + 1.0) * (2.0 * cycle - 1.0) / (cycle * period as f64 - 1.0)
1069        };
1070        let beta = (std::f64::consts::PI * t).cos();
1071        let mut g = 1.0 / (coeff * t + 1.0);
1072        if t <= 0.5 {
1073            g = 1.0;
1074        }
1075        let weight = g * beta;
1076        weights[k] = weight;
1077        weight_sum += weight;
1078    }
1079    (weights, weight_sum)
1080}
1081
1082#[inline(always)]
1083fn nonlag_ma_series(data: &[f64], period: usize) -> Vec<f64> {
1084    let (weights, weight_sum) = build_nonlag_weights(period);
1085    let len = weights.len();
1086    let mut out = vec![f64::NAN; data.len()];
1087    for_each_finite_segment(data, |start, end| {
1088        if end - start < len {
1089            return;
1090        }
1091        for i in (start + len - 1)..end {
1092            let mut sum = 0.0;
1093            let mut valid = true;
1094            for (k, &weight) in weights.iter().enumerate() {
1095                let value = data[i - k];
1096                if !value.is_finite() {
1097                    valid = false;
1098                    break;
1099                }
1100                sum += weight * value;
1101            }
1102            if valid {
1103                out[i] = sum / weight_sum;
1104            }
1105        }
1106    });
1107    out
1108}
1109
1110#[inline(always)]
1111fn percentile_nearest_rank(sorted: &[f64], probability: f64) -> f64 {
1112    if sorted.is_empty() {
1113        return f64::NAN;
1114    }
1115    let n = sorted.len();
1116    let rank = (probability * n as f64).ceil();
1117    let index = rank.max(1.0) as usize - 1;
1118    sorted[index.min(n - 1)]
1119}
1120
1121#[inline(always)]
1122fn rolling_percentile_series(data: &[f64], period: usize, probability: f64) -> Vec<f64> {
1123    let mut out = vec![f64::NAN; data.len()];
1124    for_each_finite_segment(data, |start, end| {
1125        if end - start < period {
1126            return;
1127        }
1128        let mut scratch = Vec::with_capacity(period);
1129        for i in (start + period - 1)..end {
1130            scratch.clear();
1131            scratch.extend_from_slice(&data[(i + 1 - period)..=i]);
1132            scratch.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1133            out[i] = percentile_nearest_rank(&scratch, probability);
1134        }
1135    });
1136    out
1137}
1138
1139#[inline(always)]
1140fn crossover(a_prev: f64, a: f64, b_prev: f64, b: f64) -> f64 {
1141    if a_prev.is_finite()
1142        && a.is_finite()
1143        && b_prev.is_finite()
1144        && b.is_finite()
1145        && a_prev <= b_prev
1146        && a > b
1147    {
1148        1.0
1149    } else {
1150        0.0
1151    }
1152}
1153
1154#[inline(always)]
1155fn crossunder(a_prev: f64, a: f64, b_prev: f64, b: f64) -> f64 {
1156    if a_prev.is_finite()
1157        && a.is_finite()
1158        && b_prev.is_finite()
1159        && b.is_finite()
1160        && a_prev >= b_prev
1161        && a < b
1162    {
1163        1.0
1164    } else {
1165        0.0
1166    }
1167}
1168
1169fn compute_possible_rsi_output(
1170    data: &[f64],
1171    params: PossibleRsiResolved,
1172) -> Result<PossibleRsiOutput, PossibleRsiError> {
1173    let source = if params.run_highpass {
1174        highpass_series(data, params.highpass_period)
1175    } else {
1176        data.to_vec()
1177    };
1178    let rsi = compute_rsi_series(&source, params.rsi_mode, params.period)?;
1179    let scaled = normalize_min_max(&rsi, params.norm_period);
1180    let normalized = apply_secondary_normalization(
1181        &scaled,
1182        params.normalization_mode,
1183        params.normalization_length,
1184    );
1185    let value = nonlag_ma_series(&normalized, params.nonlag_period);
1186    let buy_level =
1187        rolling_percentile_series(&value, params.dynamic_zone_period, params.buy_probability);
1188    let sell_level = rolling_percentile_series(
1189        &value,
1190        params.dynamic_zone_period,
1191        1.0 - params.sell_probability,
1192    );
1193    let middle_level = rolling_percentile_series(&value, params.dynamic_zone_period, 0.5);
1194
1195    let mut state = vec![f64::NAN; data.len()];
1196    let mut long_signal = vec![0.0; data.len()];
1197    let mut short_signal = vec![0.0; data.len()];
1198
1199    for i in 0..data.len() {
1200        if !value[i].is_finite() {
1201            continue;
1202        }
1203        let signal_value = match params.signal_type {
1204            PossibleRsiSignalType::Slope => {
1205                if i == 0 || !value[i - 1].is_finite() {
1206                    continue;
1207                }
1208                value[i - 1]
1209            }
1210            PossibleRsiSignalType::DynamicMiddleCrossover => {
1211                if !middle_level[i].is_finite() {
1212                    continue;
1213                }
1214                middle_level[i]
1215            }
1216            PossibleRsiSignalType::LevelsCrossover => {
1217                if !buy_level[i].is_finite() || !sell_level[i].is_finite() {
1218                    continue;
1219                }
1220                f64::NAN
1221            }
1222            PossibleRsiSignalType::ZerolineCrossover => 0.0,
1223        };
1224
1225        state[i] = match params.signal_type {
1226            PossibleRsiSignalType::Slope
1227            | PossibleRsiSignalType::DynamicMiddleCrossover
1228            | PossibleRsiSignalType::ZerolineCrossover => {
1229                if value[i] < signal_value {
1230                    -1.0
1231                } else if value[i] > signal_value {
1232                    1.0
1233                } else {
1234                    0.0
1235                }
1236            }
1237            PossibleRsiSignalType::LevelsCrossover => {
1238                if value[i] < buy_level[i] {
1239                    -1.0
1240                } else if value[i] > sell_level[i] {
1241                    1.0
1242                } else {
1243                    0.0
1244                }
1245            }
1246        };
1247
1248        if i == 0 {
1249            continue;
1250        }
1251
1252        match params.signal_type {
1253            PossibleRsiSignalType::Slope => {
1254                long_signal[i] = crossover(
1255                    value[i - 1],
1256                    value[i],
1257                    if i > 1 { value[i - 2] } else { value[i - 1] },
1258                    value[i - 1],
1259                );
1260                short_signal[i] = crossunder(
1261                    value[i - 1],
1262                    value[i],
1263                    if i > 1 { value[i - 2] } else { value[i - 1] },
1264                    value[i - 1],
1265                );
1266            }
1267            PossibleRsiSignalType::DynamicMiddleCrossover => {
1268                long_signal[i] =
1269                    crossover(value[i - 1], value[i], middle_level[i - 1], middle_level[i]);
1270                short_signal[i] =
1271                    crossunder(value[i - 1], value[i], middle_level[i - 1], middle_level[i]);
1272            }
1273            PossibleRsiSignalType::LevelsCrossover => {
1274                long_signal[i] =
1275                    crossover(value[i - 1], value[i], sell_level[i - 1], sell_level[i]);
1276                short_signal[i] =
1277                    crossunder(value[i - 1], value[i], buy_level[i - 1], buy_level[i]);
1278            }
1279            PossibleRsiSignalType::ZerolineCrossover => {
1280                long_signal[i] = crossover(value[i - 1], value[i], 0.0, 0.0);
1281                short_signal[i] = crossunder(value[i - 1], value[i], 0.0, 0.0);
1282            }
1283        }
1284    }
1285
1286    Ok(PossibleRsiOutput {
1287        value,
1288        buy_level,
1289        sell_level,
1290        middle_level,
1291        state,
1292        long_signal,
1293        short_signal,
1294    })
1295}
1296
1297pub fn possible_rsi(input: &PossibleRsiInput) -> Result<PossibleRsiOutput, PossibleRsiError> {
1298    possible_rsi_with_kernel(input, Kernel::Auto)
1299}
1300
1301pub fn possible_rsi_with_kernel(
1302    input: &PossibleRsiInput,
1303    kernel: Kernel,
1304) -> Result<PossibleRsiOutput, PossibleRsiError> {
1305    let data = input_slice(input);
1306    let params = resolve_params(&input.params)?;
1307    validate_common(data, params)?;
1308    let _chosen = match kernel {
1309        Kernel::Auto => detect_best_kernel(),
1310        other => other,
1311    };
1312    compute_possible_rsi_output(data, params)
1313}
1314
1315pub fn possible_rsi_into_slice(
1316    dst_value: &mut [f64],
1317    dst_buy_level: &mut [f64],
1318    dst_sell_level: &mut [f64],
1319    dst_middle_level: &mut [f64],
1320    dst_state: &mut [f64],
1321    dst_long_signal: &mut [f64],
1322    dst_short_signal: &mut [f64],
1323    input: &PossibleRsiInput,
1324    kernel: Kernel,
1325) -> Result<(), PossibleRsiError> {
1326    let data = input_slice(input);
1327    let params = resolve_params(&input.params)?;
1328    validate_common(data, params)?;
1329    if dst_value.len() != data.len()
1330        || dst_buy_level.len() != data.len()
1331        || dst_sell_level.len() != data.len()
1332        || dst_middle_level.len() != data.len()
1333        || dst_state.len() != data.len()
1334        || dst_long_signal.len() != data.len()
1335        || dst_short_signal.len() != data.len()
1336    {
1337        return Err(PossibleRsiError::OutputLengthMismatch {
1338            expected: data.len(),
1339            got: dst_value
1340                .len()
1341                .min(dst_buy_level.len())
1342                .min(dst_sell_level.len())
1343                .min(dst_middle_level.len())
1344                .min(dst_state.len())
1345                .min(dst_long_signal.len())
1346                .min(dst_short_signal.len()),
1347        });
1348    }
1349    let _chosen = match kernel {
1350        Kernel::Auto => detect_best_kernel(),
1351        other => other,
1352    };
1353    let out = compute_possible_rsi_output(data, params)?;
1354    dst_value.copy_from_slice(&out.value);
1355    dst_buy_level.copy_from_slice(&out.buy_level);
1356    dst_sell_level.copy_from_slice(&out.sell_level);
1357    dst_middle_level.copy_from_slice(&out.middle_level);
1358    dst_state.copy_from_slice(&out.state);
1359    dst_long_signal.copy_from_slice(&out.long_signal);
1360    dst_short_signal.copy_from_slice(&out.short_signal);
1361    Ok(())
1362}
1363
1364#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1365pub fn possible_rsi_into(
1366    input: &PossibleRsiInput,
1367    dst_value: &mut [f64],
1368    dst_buy_level: &mut [f64],
1369    dst_sell_level: &mut [f64],
1370    dst_middle_level: &mut [f64],
1371    dst_state: &mut [f64],
1372    dst_long_signal: &mut [f64],
1373    dst_short_signal: &mut [f64],
1374) -> Result<(), PossibleRsiError> {
1375    possible_rsi_into_slice(
1376        dst_value,
1377        dst_buy_level,
1378        dst_sell_level,
1379        dst_middle_level,
1380        dst_state,
1381        dst_long_signal,
1382        dst_short_signal,
1383        input,
1384        Kernel::Auto,
1385    )
1386}
1387
1388#[derive(Debug, Clone, Copy)]
1389pub struct PossibleRsiBatchRange {
1390    pub period: (usize, usize, usize),
1391    pub norm_period: (usize, usize, usize),
1392    pub normalization_length: (usize, usize, usize),
1393    pub nonlag_period: (usize, usize, usize),
1394    pub dynamic_zone_period: (usize, usize, usize),
1395    pub buy_probability: (f64, f64, f64),
1396    pub sell_probability: (f64, f64, f64),
1397    pub highpass_period: (usize, usize, usize),
1398}
1399
1400impl Default for PossibleRsiBatchRange {
1401    fn default() -> Self {
1402        Self {
1403            period: (32, 32, 0),
1404            norm_period: (100, 100, 0),
1405            normalization_length: (15, 15, 0),
1406            nonlag_period: (15, 15, 0),
1407            dynamic_zone_period: (20, 20, 0),
1408            buy_probability: (0.2, 0.2, 0.0),
1409            sell_probability: (0.2, 0.2, 0.0),
1410            highpass_period: (15, 15, 0),
1411        }
1412    }
1413}
1414
1415#[derive(Debug, Clone)]
1416pub struct PossibleRsiBatchOutput {
1417    pub value: Vec<f64>,
1418    pub buy_level: Vec<f64>,
1419    pub sell_level: Vec<f64>,
1420    pub middle_level: Vec<f64>,
1421    pub state: Vec<f64>,
1422    pub long_signal: Vec<f64>,
1423    pub short_signal: Vec<f64>,
1424    pub combos: Vec<PossibleRsiParams>,
1425    pub rows: usize,
1426    pub cols: usize,
1427}
1428
1429#[derive(Debug, Clone, Copy)]
1430pub struct PossibleRsiBatchBuilder {
1431    range: PossibleRsiBatchRange,
1432    rsi_mode: Option<&'static str>,
1433    normalization_mode: Option<&'static str>,
1434    signal_type: Option<&'static str>,
1435    run_highpass: Option<bool>,
1436    kernel: Kernel,
1437}
1438
1439impl Default for PossibleRsiBatchBuilder {
1440    fn default() -> Self {
1441        Self {
1442            range: PossibleRsiBatchRange::default(),
1443            rsi_mode: None,
1444            normalization_mode: None,
1445            signal_type: None,
1446            run_highpass: None,
1447            kernel: Kernel::Auto,
1448        }
1449    }
1450}
1451
1452impl PossibleRsiBatchBuilder {
1453    #[inline(always)]
1454    pub fn new() -> Self {
1455        Self::default()
1456    }
1457
1458    #[inline(always)]
1459    pub fn kernel(mut self, value: Kernel) -> Self {
1460        self.kernel = value;
1461        self
1462    }
1463
1464    #[inline(always)]
1465    pub fn rsi_mode(mut self, value: &'static str) -> Self {
1466        self.rsi_mode = Some(value);
1467        self
1468    }
1469
1470    #[inline(always)]
1471    pub fn normalization_mode(mut self, value: &'static str) -> Self {
1472        self.normalization_mode = Some(value);
1473        self
1474    }
1475
1476    #[inline(always)]
1477    pub fn signal_type(mut self, value: &'static str) -> Self {
1478        self.signal_type = Some(value);
1479        self
1480    }
1481
1482    #[inline(always)]
1483    pub fn run_highpass(mut self, value: bool) -> Self {
1484        self.run_highpass = Some(value);
1485        self
1486    }
1487
1488    #[inline(always)]
1489    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1490        self.range.period = (start, end, step);
1491        self
1492    }
1493
1494    #[inline(always)]
1495    pub fn norm_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1496        self.range.norm_period = (start, end, step);
1497        self
1498    }
1499
1500    #[inline(always)]
1501    pub fn normalization_length_range(mut self, start: usize, end: usize, step: usize) -> Self {
1502        self.range.normalization_length = (start, end, step);
1503        self
1504    }
1505
1506    #[inline(always)]
1507    pub fn nonlag_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1508        self.range.nonlag_period = (start, end, step);
1509        self
1510    }
1511
1512    #[inline(always)]
1513    pub fn dynamic_zone_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1514        self.range.dynamic_zone_period = (start, end, step);
1515        self
1516    }
1517
1518    #[inline(always)]
1519    pub fn buy_probability_range(mut self, start: f64, end: f64, step: f64) -> Self {
1520        self.range.buy_probability = (start, end, step);
1521        self
1522    }
1523
1524    #[inline(always)]
1525    pub fn sell_probability_range(mut self, start: f64, end: f64, step: f64) -> Self {
1526        self.range.sell_probability = (start, end, step);
1527        self
1528    }
1529
1530    #[inline(always)]
1531    pub fn highpass_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1532        self.range.highpass_period = (start, end, step);
1533        self
1534    }
1535
1536    #[inline(always)]
1537    fn base_params(self) -> PossibleRsiParams {
1538        PossibleRsiParams {
1539            period: None,
1540            rsi_mode: self.rsi_mode.map(str::to_string),
1541            norm_period: None,
1542            normalization_mode: self.normalization_mode.map(str::to_string),
1543            normalization_length: None,
1544            nonlag_period: None,
1545            dynamic_zone_period: None,
1546            buy_probability: None,
1547            sell_probability: None,
1548            signal_type: self.signal_type.map(str::to_string),
1549            run_highpass: self.run_highpass,
1550            highpass_period: None,
1551        }
1552    }
1553
1554    #[inline(always)]
1555    pub fn apply_slice(self, data: &[f64]) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
1556        possible_rsi_batch_with_kernel(data, &self.range, &self.base_params(), self.kernel)
1557    }
1558
1559    #[inline(always)]
1560    pub fn apply_candles(
1561        self,
1562        candles: &Candles,
1563        source: &str,
1564    ) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
1565        possible_rsi_batch_with_kernel(
1566            source_type(candles, source),
1567            &self.range,
1568            &self.base_params(),
1569            self.kernel,
1570        )
1571    }
1572}
1573
1574#[inline(always)]
1575fn expand_usize_range(
1576    field: &'static str,
1577    start: usize,
1578    end: usize,
1579    step: usize,
1580) -> Result<Vec<usize>, PossibleRsiError> {
1581    if start == 0 || end == 0 {
1582        return Err(PossibleRsiError::InvalidRange {
1583            field,
1584            start: start.to_string(),
1585            end: end.to_string(),
1586            step: step.to_string(),
1587        });
1588    }
1589    if step == 0 {
1590        return Ok(vec![start]);
1591    }
1592    if start > end {
1593        return Err(PossibleRsiError::InvalidRange {
1594            field,
1595            start: start.to_string(),
1596            end: end.to_string(),
1597            step: step.to_string(),
1598        });
1599    }
1600    let mut out = Vec::new();
1601    let mut current = start;
1602    loop {
1603        out.push(current);
1604        if current >= end {
1605            break;
1606        }
1607        let next = current.saturating_add(step);
1608        if next <= current {
1609            return Err(PossibleRsiError::InvalidRange {
1610                field,
1611                start: start.to_string(),
1612                end: end.to_string(),
1613                step: step.to_string(),
1614            });
1615        }
1616        current = next.min(end);
1617        if current == *out.last().unwrap() {
1618            break;
1619        }
1620    }
1621    Ok(out)
1622}
1623
1624#[inline(always)]
1625fn expand_float_range(
1626    field: &'static str,
1627    start: f64,
1628    end: f64,
1629    step: f64,
1630) -> Result<Vec<f64>, PossibleRsiError> {
1631    if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1632        return Err(PossibleRsiError::InvalidRange {
1633            field,
1634            start: start.to_string(),
1635            end: end.to_string(),
1636            step: step.to_string(),
1637        });
1638    }
1639    if step == 0.0 {
1640        return Ok(vec![start]);
1641    }
1642    if start > end || step < 0.0 {
1643        return Err(PossibleRsiError::InvalidRange {
1644            field,
1645            start: start.to_string(),
1646            end: end.to_string(),
1647            step: step.to_string(),
1648        });
1649    }
1650    let mut out = Vec::new();
1651    let mut current = start;
1652    loop {
1653        out.push(current);
1654        if current >= end || (end - current).abs() <= 1e-12 {
1655            break;
1656        }
1657        let next = current + step;
1658        if next <= current {
1659            return Err(PossibleRsiError::InvalidRange {
1660                field,
1661                start: start.to_string(),
1662                end: end.to_string(),
1663                step: step.to_string(),
1664            });
1665        }
1666        current = if next > end { end } else { next };
1667    }
1668    Ok(out)
1669}
1670
1671fn expand_grid_checked(
1672    range: &PossibleRsiBatchRange,
1673    base: &PossibleRsiParams,
1674) -> Result<Vec<PossibleRsiParams>, PossibleRsiError> {
1675    let periods = expand_usize_range("period", range.period.0, range.period.1, range.period.2)?;
1676    let norm_periods = expand_usize_range(
1677        "norm_period",
1678        range.norm_period.0,
1679        range.norm_period.1,
1680        range.norm_period.2,
1681    )?;
1682    let normalization_lengths = expand_usize_range(
1683        "normalization_length",
1684        range.normalization_length.0,
1685        range.normalization_length.1,
1686        range.normalization_length.2,
1687    )?;
1688    let nonlag_periods = expand_usize_range(
1689        "nonlag_period",
1690        range.nonlag_period.0,
1691        range.nonlag_period.1,
1692        range.nonlag_period.2,
1693    )?;
1694    let dynamic_zone_periods = expand_usize_range(
1695        "dynamic_zone_period",
1696        range.dynamic_zone_period.0,
1697        range.dynamic_zone_period.1,
1698        range.dynamic_zone_period.2,
1699    )?;
1700    let buy_probabilities = expand_float_range(
1701        "buy_probability",
1702        range.buy_probability.0,
1703        range.buy_probability.1,
1704        range.buy_probability.2,
1705    )?;
1706    let sell_probabilities = expand_float_range(
1707        "sell_probability",
1708        range.sell_probability.0,
1709        range.sell_probability.1,
1710        range.sell_probability.2,
1711    )?;
1712    let highpass_periods = expand_usize_range(
1713        "highpass_period",
1714        range.highpass_period.0,
1715        range.highpass_period.1,
1716        range.highpass_period.2,
1717    )?;
1718
1719    let mut combos = Vec::new();
1720    for &period in &periods {
1721        for &norm_period in &norm_periods {
1722            for &normalization_length in &normalization_lengths {
1723                for &nonlag_period in &nonlag_periods {
1724                    for &dynamic_zone_period in &dynamic_zone_periods {
1725                        for &buy_probability in &buy_probabilities {
1726                            for &sell_probability in &sell_probabilities {
1727                                for &highpass_period in &highpass_periods {
1728                                    combos.push(PossibleRsiParams {
1729                                        period: Some(period),
1730                                        rsi_mode: base.rsi_mode.clone(),
1731                                        norm_period: Some(norm_period),
1732                                        normalization_mode: base.normalization_mode.clone(),
1733                                        normalization_length: Some(normalization_length),
1734                                        nonlag_period: Some(nonlag_period),
1735                                        dynamic_zone_period: Some(dynamic_zone_period),
1736                                        buy_probability: Some(buy_probability),
1737                                        sell_probability: Some(sell_probability),
1738                                        signal_type: base.signal_type.clone(),
1739                                        run_highpass: base.run_highpass,
1740                                        highpass_period: Some(highpass_period),
1741                                    });
1742                                }
1743                            }
1744                        }
1745                    }
1746                }
1747            }
1748        }
1749    }
1750    Ok(combos)
1751}
1752
1753pub fn expand_grid_possible_rsi(
1754    range: &PossibleRsiBatchRange,
1755    base: &PossibleRsiParams,
1756) -> Vec<PossibleRsiParams> {
1757    expand_grid_checked(range, base).unwrap_or_default()
1758}
1759
1760#[inline(always)]
1761fn alloc_matrix(rows: usize, cols: usize, warmups: &[usize]) -> Vec<f64> {
1762    let mut matrix = make_uninit_matrix(rows, cols);
1763    init_matrix_prefixes(&mut matrix, cols, warmups);
1764    let mut out = unsafe {
1765        Vec::from_raw_parts(
1766            matrix.as_mut_ptr() as *mut f64,
1767            matrix.len(),
1768            matrix.capacity(),
1769        )
1770    };
1771    std::mem::forget(matrix);
1772    out
1773}
1774
1775pub fn possible_rsi_batch_with_kernel(
1776    data: &[f64],
1777    range: &PossibleRsiBatchRange,
1778    base: &PossibleRsiParams,
1779    kernel: Kernel,
1780) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
1781    match kernel {
1782        Kernel::Auto
1783        | Kernel::Scalar
1784        | Kernel::ScalarBatch
1785        | Kernel::Avx2
1786        | Kernel::Avx2Batch
1787        | Kernel::Avx512
1788        | Kernel::Avx512Batch => {}
1789        other => return Err(PossibleRsiError::InvalidKernelForBatch(other)),
1790    }
1791
1792    let combos = expand_grid_checked(range, base)?;
1793    if data.is_empty() {
1794        return Err(PossibleRsiError::EmptyInputData);
1795    }
1796    if longest_valid_run(data) == 0 {
1797        return Err(PossibleRsiError::AllValuesNaN);
1798    }
1799
1800    let rows = combos.len();
1801    let cols = data.len();
1802    let warmups = combos
1803        .iter()
1804        .map(|params| {
1805            resolve_params(params)
1806                .map(estimated_warmup)
1807                .unwrap_or(cols)
1808                .min(cols)
1809        })
1810        .collect::<Vec<_>>();
1811    let mut value = alloc_matrix(rows, cols, &warmups);
1812    let mut buy_level = alloc_matrix(rows, cols, &warmups);
1813    let mut sell_level = alloc_matrix(rows, cols, &warmups);
1814    let mut middle_level = alloc_matrix(rows, cols, &warmups);
1815    let mut state = alloc_matrix(rows, cols, &warmups);
1816    let mut long_signal = alloc_matrix(rows, cols, &warmups);
1817    let mut short_signal = alloc_matrix(rows, cols, &warmups);
1818
1819    let _chosen = match kernel {
1820        Kernel::Auto => detect_best_batch_kernel(),
1821        other => other,
1822    };
1823
1824    let worker = |row: usize,
1825                  dst_value: &mut [f64],
1826                  dst_buy: &mut [f64],
1827                  dst_sell: &mut [f64],
1828                  dst_middle: &mut [f64],
1829                  dst_state: &mut [f64],
1830                  dst_long: &mut [f64],
1831                  dst_short: &mut [f64]| {
1832        if let Ok(out) = possible_rsi(&PossibleRsiInput::from_slice(data, combos[row].clone())) {
1833            dst_value.copy_from_slice(&out.value);
1834            dst_buy.copy_from_slice(&out.buy_level);
1835            dst_sell.copy_from_slice(&out.sell_level);
1836            dst_middle.copy_from_slice(&out.middle_level);
1837            dst_state.copy_from_slice(&out.state);
1838            dst_long.copy_from_slice(&out.long_signal);
1839            dst_short.copy_from_slice(&out.short_signal);
1840        }
1841    };
1842
1843    #[cfg(not(target_arch = "wasm32"))]
1844    {
1845        value
1846            .par_chunks_mut(cols)
1847            .zip(buy_level.par_chunks_mut(cols))
1848            .zip(sell_level.par_chunks_mut(cols))
1849            .zip(middle_level.par_chunks_mut(cols))
1850            .zip(state.par_chunks_mut(cols))
1851            .zip(long_signal.par_chunks_mut(cols))
1852            .zip(short_signal.par_chunks_mut(cols))
1853            .enumerate()
1854            .for_each(
1855                |(
1856                    row,
1857                    (
1858                        (((((dst_value, dst_buy), dst_sell), dst_middle), dst_state), dst_long),
1859                        dst_short,
1860                    ),
1861                )| {
1862                    worker(
1863                        row, dst_value, dst_buy, dst_sell, dst_middle, dst_state, dst_long,
1864                        dst_short,
1865                    );
1866                },
1867            );
1868    }
1869
1870    #[cfg(target_arch = "wasm32")]
1871    {
1872        for (
1873            row,
1874            ((((((dst_value, dst_buy), dst_sell), dst_middle), dst_state), dst_long), dst_short),
1875        ) in value
1876            .chunks_mut(cols)
1877            .zip(buy_level.chunks_mut(cols))
1878            .zip(sell_level.chunks_mut(cols))
1879            .zip(middle_level.chunks_mut(cols))
1880            .zip(state.chunks_mut(cols))
1881            .zip(long_signal.chunks_mut(cols))
1882            .zip(short_signal.chunks_mut(cols))
1883            .enumerate()
1884        {
1885            worker(
1886                row, dst_value, dst_buy, dst_sell, dst_middle, dst_state, dst_long, dst_short,
1887            );
1888        }
1889    }
1890
1891    Ok(PossibleRsiBatchOutput {
1892        value,
1893        buy_level,
1894        sell_level,
1895        middle_level,
1896        state,
1897        long_signal,
1898        short_signal,
1899        combos,
1900        rows,
1901        cols,
1902    })
1903}
1904pub fn possible_rsi_batch_slice(
1905    data: &[f64],
1906    range: &PossibleRsiBatchRange,
1907    base: &PossibleRsiParams,
1908    kernel: Kernel,
1909) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
1910    possible_rsi_batch_with_kernel(data, range, base, kernel)
1911}
1912
1913pub fn possible_rsi_batch_par_slice(
1914    data: &[f64],
1915    range: &PossibleRsiBatchRange,
1916    base: &PossibleRsiParams,
1917    kernel: Kernel,
1918) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
1919    possible_rsi_batch_with_kernel(data, range, base, kernel)
1920}
1921
1922#[cfg(feature = "python")]
1923#[pyfunction(name = "possible_rsi")]
1924#[pyo3(signature = (data, period=32, rsi_mode="regular", norm_period=100, normalization_mode="gaussian_fisher", normalization_length=15, nonlag_period=15, dynamic_zone_period=20, buy_probability=0.2, sell_probability=0.2, signal_type="zeroline_crossover", run_highpass=false, highpass_period=15, kernel=None))]
1925pub fn possible_rsi_py<'py>(
1926    py: Python<'py>,
1927    data: PyReadonlyArray1<'py, f64>,
1928    period: usize,
1929    rsi_mode: &str,
1930    norm_period: usize,
1931    normalization_mode: &str,
1932    normalization_length: usize,
1933    nonlag_period: usize,
1934    dynamic_zone_period: usize,
1935    buy_probability: f64,
1936    sell_probability: f64,
1937    signal_type: &str,
1938    run_highpass: bool,
1939    highpass_period: usize,
1940    kernel: Option<&str>,
1941) -> PyResult<(
1942    Bound<'py, PyArray1<f64>>,
1943    Bound<'py, PyArray1<f64>>,
1944    Bound<'py, PyArray1<f64>>,
1945    Bound<'py, PyArray1<f64>>,
1946    Bound<'py, PyArray1<f64>>,
1947    Bound<'py, PyArray1<f64>>,
1948    Bound<'py, PyArray1<f64>>,
1949)> {
1950    let data = data.as_slice()?;
1951    let kern = validate_kernel(kernel, false)?;
1952    let input = PossibleRsiInput::from_slice(
1953        data,
1954        PossibleRsiParams {
1955            period: Some(period),
1956            rsi_mode: Some(rsi_mode.to_string()),
1957            norm_period: Some(norm_period),
1958            normalization_mode: Some(normalization_mode.to_string()),
1959            normalization_length: Some(normalization_length),
1960            nonlag_period: Some(nonlag_period),
1961            dynamic_zone_period: Some(dynamic_zone_period),
1962            buy_probability: Some(buy_probability),
1963            sell_probability: Some(sell_probability),
1964            signal_type: Some(signal_type.to_string()),
1965            run_highpass: Some(run_highpass),
1966            highpass_period: Some(highpass_period),
1967        },
1968    );
1969    let out = py
1970        .allow_threads(|| possible_rsi_with_kernel(&input, kern))
1971        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1972    Ok((
1973        out.value.into_pyarray(py),
1974        out.buy_level.into_pyarray(py),
1975        out.sell_level.into_pyarray(py),
1976        out.middle_level.into_pyarray(py),
1977        out.state.into_pyarray(py),
1978        out.long_signal.into_pyarray(py),
1979        out.short_signal.into_pyarray(py),
1980    ))
1981}
1982
1983#[cfg(feature = "python")]
1984#[pyclass(name = "PossibleRsiStream")]
1985pub struct PossibleRsiStreamPy {
1986    stream: PossibleRsiStream,
1987}
1988
1989#[cfg(feature = "python")]
1990#[pymethods]
1991impl PossibleRsiStreamPy {
1992    #[new]
1993    #[pyo3(signature = (period=32, rsi_mode="regular", norm_period=100, normalization_mode="gaussian_fisher", normalization_length=15, nonlag_period=15, dynamic_zone_period=20, buy_probability=0.2, sell_probability=0.2, signal_type="zeroline_crossover", run_highpass=false, highpass_period=15))]
1994    fn new(
1995        period: usize,
1996        rsi_mode: &str,
1997        norm_period: usize,
1998        normalization_mode: &str,
1999        normalization_length: usize,
2000        nonlag_period: usize,
2001        dynamic_zone_period: usize,
2002        buy_probability: f64,
2003        sell_probability: f64,
2004        signal_type: &str,
2005        run_highpass: bool,
2006        highpass_period: usize,
2007    ) -> PyResult<Self> {
2008        let stream = PossibleRsiStream::try_new(PossibleRsiParams {
2009            period: Some(period),
2010            rsi_mode: Some(rsi_mode.to_string()),
2011            norm_period: Some(norm_period),
2012            normalization_mode: Some(normalization_mode.to_string()),
2013            normalization_length: Some(normalization_length),
2014            nonlag_period: Some(nonlag_period),
2015            dynamic_zone_period: Some(dynamic_zone_period),
2016            buy_probability: Some(buy_probability),
2017            sell_probability: Some(sell_probability),
2018            signal_type: Some(signal_type.to_string()),
2019            run_highpass: Some(run_highpass),
2020            highpass_period: Some(highpass_period),
2021        })
2022        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2023        Ok(Self { stream })
2024    }
2025
2026    fn update(&mut self, value: f64) -> Option<(f64, f64, f64, f64, f64, f64, f64)> {
2027        self.stream.update(value).map(|point| {
2028            (
2029                point.value,
2030                point.buy_level,
2031                point.sell_level,
2032                point.middle_level,
2033                point.state,
2034                point.long_signal,
2035                point.short_signal,
2036            )
2037        })
2038    }
2039
2040    fn reset(&mut self) {
2041        self.stream.reset();
2042    }
2043
2044    #[getter]
2045    fn warmup_period(&self) -> usize {
2046        self.stream.get_warmup_period()
2047    }
2048}
2049
2050#[cfg(feature = "python")]
2051#[pyfunction(name = "possible_rsi_batch")]
2052#[pyo3(signature = (data, period_range=(32, 32, 0), rsi_mode="regular", norm_period_range=(100, 100, 0), normalization_mode="gaussian_fisher", normalization_length_range=(15, 15, 0), nonlag_period_range=(15, 15, 0), dynamic_zone_period_range=(20, 20, 0), buy_probability_range=(0.2, 0.2, 0.0), sell_probability_range=(0.2, 0.2, 0.0), signal_type="zeroline_crossover", run_highpass=false, highpass_period=15, kernel=None))]
2053pub fn possible_rsi_batch_py<'py>(
2054    py: Python<'py>,
2055    data: PyReadonlyArray1<'py, f64>,
2056    period_range: (usize, usize, usize),
2057    rsi_mode: &str,
2058    norm_period_range: (usize, usize, usize),
2059    normalization_mode: &str,
2060    normalization_length_range: (usize, usize, usize),
2061    nonlag_period_range: (usize, usize, usize),
2062    dynamic_zone_period_range: (usize, usize, usize),
2063    buy_probability_range: (f64, f64, f64),
2064    sell_probability_range: (f64, f64, f64),
2065    signal_type: &str,
2066    run_highpass: bool,
2067    highpass_period: usize,
2068    kernel: Option<&str>,
2069) -> PyResult<Bound<'py, PyDict>> {
2070    let data = data.as_slice()?;
2071    let kern = validate_kernel(kernel, true)?;
2072    let output = py
2073        .allow_threads(|| {
2074            possible_rsi_batch_with_kernel(
2075                data,
2076                &PossibleRsiBatchRange {
2077                    period: period_range,
2078                    norm_period: norm_period_range,
2079                    normalization_length: normalization_length_range,
2080                    nonlag_period: nonlag_period_range,
2081                    dynamic_zone_period: dynamic_zone_period_range,
2082                    buy_probability: buy_probability_range,
2083                    sell_probability: sell_probability_range,
2084                    highpass_period: (highpass_period, highpass_period, 0),
2085                },
2086                &PossibleRsiParams {
2087                    period: None,
2088                    rsi_mode: Some(rsi_mode.to_string()),
2089                    norm_period: None,
2090                    normalization_mode: Some(normalization_mode.to_string()),
2091                    normalization_length: None,
2092                    nonlag_period: None,
2093                    dynamic_zone_period: None,
2094                    buy_probability: None,
2095                    sell_probability: None,
2096                    signal_type: Some(signal_type.to_string()),
2097                    run_highpass: Some(run_highpass),
2098                    highpass_period: Some(highpass_period),
2099                },
2100                kern,
2101            )
2102        })
2103        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2104
2105    let dict = PyDict::new(py);
2106    dict.set_item(
2107        "value",
2108        output
2109            .value
2110            .into_pyarray(py)
2111            .reshape((output.rows, output.cols))?,
2112    )?;
2113    dict.set_item(
2114        "buy_level",
2115        output
2116            .buy_level
2117            .into_pyarray(py)
2118            .reshape((output.rows, output.cols))?,
2119    )?;
2120    dict.set_item(
2121        "sell_level",
2122        output
2123            .sell_level
2124            .into_pyarray(py)
2125            .reshape((output.rows, output.cols))?,
2126    )?;
2127    dict.set_item(
2128        "middle_level",
2129        output
2130            .middle_level
2131            .into_pyarray(py)
2132            .reshape((output.rows, output.cols))?,
2133    )?;
2134    dict.set_item(
2135        "state",
2136        output
2137            .state
2138            .into_pyarray(py)
2139            .reshape((output.rows, output.cols))?,
2140    )?;
2141    dict.set_item(
2142        "long_signal",
2143        output
2144            .long_signal
2145            .into_pyarray(py)
2146            .reshape((output.rows, output.cols))?,
2147    )?;
2148    dict.set_item(
2149        "short_signal",
2150        output
2151            .short_signal
2152            .into_pyarray(py)
2153            .reshape((output.rows, output.cols))?,
2154    )?;
2155    dict.set_item(
2156        "periods",
2157        output
2158            .combos
2159            .iter()
2160            .map(|combo| combo.period.unwrap_or(32) as u64)
2161            .collect::<Vec<_>>()
2162            .into_pyarray(py),
2163    )?;
2164    dict.set_item(
2165        "norm_periods",
2166        output
2167            .combos
2168            .iter()
2169            .map(|combo| combo.norm_period.unwrap_or(100) as u64)
2170            .collect::<Vec<_>>()
2171            .into_pyarray(py),
2172    )?;
2173    dict.set_item(
2174        "normalization_lengths",
2175        output
2176            .combos
2177            .iter()
2178            .map(|combo| combo.normalization_length.unwrap_or(15) as u64)
2179            .collect::<Vec<_>>()
2180            .into_pyarray(py),
2181    )?;
2182    dict.set_item(
2183        "nonlag_periods",
2184        output
2185            .combos
2186            .iter()
2187            .map(|combo| combo.nonlag_period.unwrap_or(15) as u64)
2188            .collect::<Vec<_>>()
2189            .into_pyarray(py),
2190    )?;
2191    dict.set_item(
2192        "dynamic_zone_periods",
2193        output
2194            .combos
2195            .iter()
2196            .map(|combo| combo.dynamic_zone_period.unwrap_or(20) as u64)
2197            .collect::<Vec<_>>()
2198            .into_pyarray(py),
2199    )?;
2200    dict.set_item(
2201        "buy_probabilities",
2202        output
2203            .combos
2204            .iter()
2205            .map(|combo| combo.buy_probability.unwrap_or(0.2))
2206            .collect::<Vec<_>>()
2207            .into_pyarray(py),
2208    )?;
2209    dict.set_item(
2210        "sell_probabilities",
2211        output
2212            .combos
2213            .iter()
2214            .map(|combo| combo.sell_probability.unwrap_or(0.2))
2215            .collect::<Vec<_>>()
2216            .into_pyarray(py),
2217    )?;
2218    dict.set_item(
2219        "rsi_modes",
2220        output
2221            .combos
2222            .iter()
2223            .map(|combo| {
2224                combo
2225                    .rsi_mode
2226                    .clone()
2227                    .unwrap_or_else(|| "regular".to_string())
2228            })
2229            .collect::<Vec<_>>(),
2230    )?;
2231    dict.set_item(
2232        "normalization_modes",
2233        output
2234            .combos
2235            .iter()
2236            .map(|combo| {
2237                combo
2238                    .normalization_mode
2239                    .clone()
2240                    .unwrap_or_else(|| "gaussian_fisher".to_string())
2241            })
2242            .collect::<Vec<_>>(),
2243    )?;
2244    dict.set_item(
2245        "signal_types",
2246        output
2247            .combos
2248            .iter()
2249            .map(|combo| {
2250                combo
2251                    .signal_type
2252                    .clone()
2253                    .unwrap_or_else(|| "zeroline_crossover".to_string())
2254            })
2255            .collect::<Vec<_>>(),
2256    )?;
2257    dict.set_item(
2258        "run_highpass",
2259        output
2260            .combos
2261            .iter()
2262            .map(|combo| combo.run_highpass.unwrap_or(false))
2263            .collect::<Vec<_>>(),
2264    )?;
2265    dict.set_item("rows", output.rows)?;
2266    dict.set_item("cols", output.cols)?;
2267    Ok(dict)
2268}
2269
2270#[cfg(feature = "python")]
2271pub fn register_possible_rsi_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
2272    m.add_function(wrap_pyfunction!(possible_rsi_py, m)?)?;
2273    m.add_function(wrap_pyfunction!(possible_rsi_batch_py, m)?)?;
2274    m.add_class::<PossibleRsiStreamPy>()?;
2275    Ok(())
2276}
2277
2278#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2279#[derive(Debug, Clone, Serialize, Deserialize)]
2280pub struct PossibleRsiBatchConfig {
2281    pub period_range: Vec<usize>,
2282    pub rsi_mode: Option<String>,
2283    pub norm_period_range: Vec<usize>,
2284    pub normalization_mode: Option<String>,
2285    pub normalization_length_range: Vec<usize>,
2286    pub nonlag_period_range: Vec<usize>,
2287    pub dynamic_zone_period_range: Vec<usize>,
2288    pub buy_probability_range: Vec<f64>,
2289    pub sell_probability_range: Vec<f64>,
2290    pub signal_type: Option<String>,
2291    pub run_highpass: Option<bool>,
2292    pub highpass_period: Option<usize>,
2293}
2294
2295#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2296#[wasm_bindgen(js_name = possible_rsi_js)]
2297pub fn possible_rsi_js(
2298    data: &[f64],
2299    period: usize,
2300    rsi_mode: &str,
2301    norm_period: usize,
2302    normalization_mode: &str,
2303    normalization_length: usize,
2304    nonlag_period: usize,
2305    dynamic_zone_period: usize,
2306    buy_probability: f64,
2307    sell_probability: f64,
2308    signal_type: &str,
2309    run_highpass: bool,
2310    highpass_period: usize,
2311) -> Result<JsValue, JsValue> {
2312    let input = PossibleRsiInput::from_slice(
2313        data,
2314        PossibleRsiParams {
2315            period: Some(period),
2316            rsi_mode: Some(rsi_mode.to_string()),
2317            norm_period: Some(norm_period),
2318            normalization_mode: Some(normalization_mode.to_string()),
2319            normalization_length: Some(normalization_length),
2320            nonlag_period: Some(nonlag_period),
2321            dynamic_zone_period: Some(dynamic_zone_period),
2322            buy_probability: Some(buy_probability),
2323            sell_probability: Some(sell_probability),
2324            signal_type: Some(signal_type.to_string()),
2325            run_highpass: Some(run_highpass),
2326            highpass_period: Some(highpass_period),
2327        },
2328    );
2329    let out = possible_rsi_with_kernel(&input, Kernel::Auto)
2330        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2331    let obj = js_sys::Object::new();
2332    js_sys::Reflect::set(
2333        &obj,
2334        &JsValue::from_str("value"),
2335        &serde_wasm_bindgen::to_value(&out.value).unwrap(),
2336    )?;
2337    js_sys::Reflect::set(
2338        &obj,
2339        &JsValue::from_str("buy_level"),
2340        &serde_wasm_bindgen::to_value(&out.buy_level).unwrap(),
2341    )?;
2342    js_sys::Reflect::set(
2343        &obj,
2344        &JsValue::from_str("sell_level"),
2345        &serde_wasm_bindgen::to_value(&out.sell_level).unwrap(),
2346    )?;
2347    js_sys::Reflect::set(
2348        &obj,
2349        &JsValue::from_str("middle_level"),
2350        &serde_wasm_bindgen::to_value(&out.middle_level).unwrap(),
2351    )?;
2352    js_sys::Reflect::set(
2353        &obj,
2354        &JsValue::from_str("state"),
2355        &serde_wasm_bindgen::to_value(&out.state).unwrap(),
2356    )?;
2357    js_sys::Reflect::set(
2358        &obj,
2359        &JsValue::from_str("long_signal"),
2360        &serde_wasm_bindgen::to_value(&out.long_signal).unwrap(),
2361    )?;
2362    js_sys::Reflect::set(
2363        &obj,
2364        &JsValue::from_str("short_signal"),
2365        &serde_wasm_bindgen::to_value(&out.short_signal).unwrap(),
2366    )?;
2367    Ok(obj.into())
2368}
2369
2370#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2371#[wasm_bindgen(js_name = possible_rsi_batch_js)]
2372pub fn possible_rsi_batch_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
2373    let config: PossibleRsiBatchConfig = serde_wasm_bindgen::from_value(config)
2374        .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2375    if config.period_range.len() != 3
2376        || config.norm_period_range.len() != 3
2377        || config.normalization_length_range.len() != 3
2378        || config.nonlag_period_range.len() != 3
2379        || config.dynamic_zone_period_range.len() != 3
2380        || config.buy_probability_range.len() != 3
2381        || config.sell_probability_range.len() != 3
2382    {
2383        return Err(JsValue::from_str(
2384            "Invalid config: every range must have exactly 3 elements [start, end, step]",
2385        ));
2386    }
2387    let highpass_period = config.highpass_period.unwrap_or(15);
2388    let out = possible_rsi_batch_with_kernel(
2389        data,
2390        &PossibleRsiBatchRange {
2391            period: (
2392                config.period_range[0],
2393                config.period_range[1],
2394                config.period_range[2],
2395            ),
2396            norm_period: (
2397                config.norm_period_range[0],
2398                config.norm_period_range[1],
2399                config.norm_period_range[2],
2400            ),
2401            normalization_length: (
2402                config.normalization_length_range[0],
2403                config.normalization_length_range[1],
2404                config.normalization_length_range[2],
2405            ),
2406            nonlag_period: (
2407                config.nonlag_period_range[0],
2408                config.nonlag_period_range[1],
2409                config.nonlag_period_range[2],
2410            ),
2411            dynamic_zone_period: (
2412                config.dynamic_zone_period_range[0],
2413                config.dynamic_zone_period_range[1],
2414                config.dynamic_zone_period_range[2],
2415            ),
2416            buy_probability: (
2417                config.buy_probability_range[0],
2418                config.buy_probability_range[1],
2419                config.buy_probability_range[2],
2420            ),
2421            sell_probability: (
2422                config.sell_probability_range[0],
2423                config.sell_probability_range[1],
2424                config.sell_probability_range[2],
2425            ),
2426            highpass_period: (highpass_period, highpass_period, 0),
2427        },
2428        &PossibleRsiParams {
2429            period: None,
2430            rsi_mode: Some(config.rsi_mode.unwrap_or_else(|| "regular".to_string())),
2431            norm_period: None,
2432            normalization_mode: Some(
2433                config
2434                    .normalization_mode
2435                    .unwrap_or_else(|| "gaussian_fisher".to_string()),
2436            ),
2437            normalization_length: None,
2438            nonlag_period: None,
2439            dynamic_zone_period: None,
2440            buy_probability: None,
2441            sell_probability: None,
2442            signal_type: Some(
2443                config
2444                    .signal_type
2445                    .unwrap_or_else(|| "zeroline_crossover".to_string()),
2446            ),
2447            run_highpass: Some(config.run_highpass.unwrap_or(false)),
2448            highpass_period: Some(highpass_period),
2449        },
2450        Kernel::Auto,
2451    )
2452    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2453
2454    let obj = js_sys::Object::new();
2455    js_sys::Reflect::set(
2456        &obj,
2457        &JsValue::from_str("value"),
2458        &serde_wasm_bindgen::to_value(&out.value).unwrap(),
2459    )?;
2460    js_sys::Reflect::set(
2461        &obj,
2462        &JsValue::from_str("buy_level"),
2463        &serde_wasm_bindgen::to_value(&out.buy_level).unwrap(),
2464    )?;
2465    js_sys::Reflect::set(
2466        &obj,
2467        &JsValue::from_str("sell_level"),
2468        &serde_wasm_bindgen::to_value(&out.sell_level).unwrap(),
2469    )?;
2470    js_sys::Reflect::set(
2471        &obj,
2472        &JsValue::from_str("middle_level"),
2473        &serde_wasm_bindgen::to_value(&out.middle_level).unwrap(),
2474    )?;
2475    js_sys::Reflect::set(
2476        &obj,
2477        &JsValue::from_str("state"),
2478        &serde_wasm_bindgen::to_value(&out.state).unwrap(),
2479    )?;
2480    js_sys::Reflect::set(
2481        &obj,
2482        &JsValue::from_str("long_signal"),
2483        &serde_wasm_bindgen::to_value(&out.long_signal).unwrap(),
2484    )?;
2485    js_sys::Reflect::set(
2486        &obj,
2487        &JsValue::from_str("short_signal"),
2488        &serde_wasm_bindgen::to_value(&out.short_signal).unwrap(),
2489    )?;
2490    js_sys::Reflect::set(
2491        &obj,
2492        &JsValue::from_str("rows"),
2493        &JsValue::from_f64(out.rows as f64),
2494    )?;
2495    js_sys::Reflect::set(
2496        &obj,
2497        &JsValue::from_str("cols"),
2498        &JsValue::from_f64(out.cols as f64),
2499    )?;
2500    js_sys::Reflect::set(
2501        &obj,
2502        &JsValue::from_str("combos"),
2503        &serde_wasm_bindgen::to_value(&out.combos).unwrap(),
2504    )?;
2505    Ok(obj.into())
2506}
2507
2508#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2509#[wasm_bindgen]
2510pub fn possible_rsi_alloc(len: usize) -> *mut f64 {
2511    let mut vec = Vec::<f64>::with_capacity(7 * len);
2512    let ptr = vec.as_mut_ptr();
2513    std::mem::forget(vec);
2514    ptr
2515}
2516
2517#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2518#[wasm_bindgen]
2519pub fn possible_rsi_free(ptr: *mut f64, len: usize) {
2520    if !ptr.is_null() {
2521        unsafe {
2522            let _ = Vec::from_raw_parts(ptr, 7 * len, 7 * len);
2523        }
2524    }
2525}
2526
2527#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2528#[wasm_bindgen]
2529pub fn possible_rsi_into(
2530    data_ptr: *const f64,
2531    out_ptr: *mut f64,
2532    len: usize,
2533    period: usize,
2534    rsi_mode: &str,
2535    norm_period: usize,
2536    normalization_mode: &str,
2537    normalization_length: usize,
2538    nonlag_period: usize,
2539    dynamic_zone_period: usize,
2540    buy_probability: f64,
2541    sell_probability: f64,
2542    signal_type: &str,
2543    run_highpass: bool,
2544    highpass_period: usize,
2545) -> Result<(), JsValue> {
2546    if data_ptr.is_null() || out_ptr.is_null() {
2547        return Err(JsValue::from_str(
2548            "null pointer passed to possible_rsi_into",
2549        ));
2550    }
2551    unsafe {
2552        let data = std::slice::from_raw_parts(data_ptr, len);
2553        let out = std::slice::from_raw_parts_mut(out_ptr, 7 * len);
2554        let (dst_value, rest) = out.split_at_mut(len);
2555        let (dst_buy_level, rest) = rest.split_at_mut(len);
2556        let (dst_sell_level, rest) = rest.split_at_mut(len);
2557        let (dst_middle_level, rest) = rest.split_at_mut(len);
2558        let (dst_state, rest) = rest.split_at_mut(len);
2559        let (dst_long_signal, dst_short_signal) = rest.split_at_mut(len);
2560        let input = PossibleRsiInput::from_slice(
2561            data,
2562            PossibleRsiParams {
2563                period: Some(period),
2564                rsi_mode: Some(rsi_mode.to_string()),
2565                norm_period: Some(norm_period),
2566                normalization_mode: Some(normalization_mode.to_string()),
2567                normalization_length: Some(normalization_length),
2568                nonlag_period: Some(nonlag_period),
2569                dynamic_zone_period: Some(dynamic_zone_period),
2570                buy_probability: Some(buy_probability),
2571                sell_probability: Some(sell_probability),
2572                signal_type: Some(signal_type.to_string()),
2573                run_highpass: Some(run_highpass),
2574                highpass_period: Some(highpass_period),
2575            },
2576        );
2577        possible_rsi_into_slice(
2578            dst_value,
2579            dst_buy_level,
2580            dst_sell_level,
2581            dst_middle_level,
2582            dst_state,
2583            dst_long_signal,
2584            dst_short_signal,
2585            &input,
2586            Kernel::Auto,
2587        )
2588        .map_err(|e| JsValue::from_str(&e.to_string()))
2589    }
2590}
2591
2592#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2593#[wasm_bindgen]
2594pub fn possible_rsi_batch_into(
2595    data_ptr: *const f64,
2596    out_ptr: *mut f64,
2597    len: usize,
2598    period_start: usize,
2599    period_end: usize,
2600    period_step: usize,
2601    rsi_mode: &str,
2602    norm_period_start: usize,
2603    norm_period_end: usize,
2604    norm_period_step: usize,
2605    normalization_mode: &str,
2606    normalization_length_start: usize,
2607    normalization_length_end: usize,
2608    normalization_length_step: usize,
2609    nonlag_period_start: usize,
2610    nonlag_period_end: usize,
2611    nonlag_period_step: usize,
2612    dynamic_zone_period_start: usize,
2613    dynamic_zone_period_end: usize,
2614    dynamic_zone_period_step: usize,
2615    buy_probability_start: f64,
2616    buy_probability_end: f64,
2617    buy_probability_step: f64,
2618    sell_probability_start: f64,
2619    sell_probability_end: f64,
2620    sell_probability_step: f64,
2621    signal_type: &str,
2622    run_highpass: bool,
2623    highpass_period: usize,
2624) -> Result<usize, JsValue> {
2625    if data_ptr.is_null() || out_ptr.is_null() {
2626        return Err(JsValue::from_str(
2627            "null pointer passed to possible_rsi_batch_into",
2628        ));
2629    }
2630    let batch = unsafe {
2631        let data = std::slice::from_raw_parts(data_ptr, len);
2632        possible_rsi_batch_with_kernel(
2633            data,
2634            &PossibleRsiBatchRange {
2635                period: (period_start, period_end, period_step),
2636                norm_period: (norm_period_start, norm_period_end, norm_period_step),
2637                normalization_length: (
2638                    normalization_length_start,
2639                    normalization_length_end,
2640                    normalization_length_step,
2641                ),
2642                nonlag_period: (nonlag_period_start, nonlag_period_end, nonlag_period_step),
2643                dynamic_zone_period: (
2644                    dynamic_zone_period_start,
2645                    dynamic_zone_period_end,
2646                    dynamic_zone_period_step,
2647                ),
2648                buy_probability: (
2649                    buy_probability_start,
2650                    buy_probability_end,
2651                    buy_probability_step,
2652                ),
2653                sell_probability: (
2654                    sell_probability_start,
2655                    sell_probability_end,
2656                    sell_probability_step,
2657                ),
2658                highpass_period: (highpass_period, highpass_period, 0),
2659            },
2660            &PossibleRsiParams {
2661                period: None,
2662                rsi_mode: Some(rsi_mode.to_string()),
2663                norm_period: None,
2664                normalization_mode: Some(normalization_mode.to_string()),
2665                normalization_length: None,
2666                nonlag_period: None,
2667                dynamic_zone_period: None,
2668                buy_probability: None,
2669                sell_probability: None,
2670                signal_type: Some(signal_type.to_string()),
2671                run_highpass: Some(run_highpass),
2672                highpass_period: Some(highpass_period),
2673            },
2674            Kernel::Auto,
2675        )
2676        .map_err(|e| JsValue::from_str(&e.to_string()))?
2677    };
2678    let rows = batch.rows;
2679    let total = rows
2680        .checked_mul(len)
2681        .and_then(|value| value.checked_mul(7))
2682        .ok_or_else(|| JsValue::from_str("rows*cols overflow in possible_rsi_batch_into"))?;
2683    unsafe {
2684        let out = std::slice::from_raw_parts_mut(out_ptr, total);
2685        let field_len = rows * len;
2686        let (dst_value, rest) = out.split_at_mut(field_len);
2687        let (dst_buy_level, rest) = rest.split_at_mut(field_len);
2688        let (dst_sell_level, rest) = rest.split_at_mut(field_len);
2689        let (dst_middle_level, rest) = rest.split_at_mut(field_len);
2690        let (dst_state, rest) = rest.split_at_mut(field_len);
2691        let (dst_long_signal, dst_short_signal) = rest.split_at_mut(field_len);
2692        dst_value.copy_from_slice(&batch.value);
2693        dst_buy_level.copy_from_slice(&batch.buy_level);
2694        dst_sell_level.copy_from_slice(&batch.sell_level);
2695        dst_middle_level.copy_from_slice(&batch.middle_level);
2696        dst_state.copy_from_slice(&batch.state);
2697        dst_long_signal.copy_from_slice(&batch.long_signal);
2698        dst_short_signal.copy_from_slice(&batch.short_signal);
2699    }
2700    Ok(rows)
2701}
2702
2703#[cfg(test)]
2704mod tests {
2705    use super::*;
2706    use crate::indicators::dispatch::{
2707        compute_cpu, compute_cpu_batch, IndicatorBatchRequest, IndicatorComputeRequest,
2708        IndicatorDataRef, IndicatorParamSet, IndicatorSeries, ParamKV, ParamValue,
2709    };
2710
2711    fn sample_close(len: usize) -> Vec<f64> {
2712        (0..len)
2713            .map(|i| {
2714                let x = i as f64;
2715                100.0 + x * 0.07 + (x * 0.13).sin() * 1.4 + (x * 0.037).cos() * 0.9
2716            })
2717            .collect()
2718    }
2719
2720    fn assert_close(left: &[f64], right: &[f64], tol: f64) {
2721        assert_eq!(left.len(), right.len());
2722        for (a, b) in left.iter().zip(right.iter()) {
2723            if a.is_nan() || b.is_nan() {
2724                assert!(a.is_nan() && b.is_nan(), "left={a} right={b}");
2725            } else {
2726                assert!((a - b).abs() <= tol, "left={a} right={b}");
2727            }
2728        }
2729    }
2730
2731    #[test]
2732    fn possible_rsi_output_contract() -> Result<(), Box<dyn Error>> {
2733        let data = sample_close(384);
2734        let out = possible_rsi(&PossibleRsiInput::from_slice(
2735            &data,
2736            PossibleRsiParams::default(),
2737        ))?;
2738        assert_eq!(out.value.len(), data.len());
2739        assert_eq!(out.buy_level.len(), data.len());
2740        assert_eq!(out.sell_level.len(), data.len());
2741        assert_eq!(out.middle_level.len(), data.len());
2742        assert_eq!(out.state.len(), data.len());
2743        assert_eq!(out.long_signal.len(), data.len());
2744        assert_eq!(out.short_signal.len(), data.len());
2745        assert!(out.value.iter().any(|v| v.is_finite()));
2746        assert!(out.value.last().is_some_and(|v| v.is_finite()));
2747        assert!(out.buy_level.last().is_some_and(|v| v.is_finite()));
2748        assert!(out.sell_level.last().is_some_and(|v| v.is_finite()));
2749        assert!(out.middle_level.last().is_some_and(|v| v.is_finite()));
2750        assert!(out.state.last().is_some_and(|v| v.is_finite()));
2751        Ok(())
2752    }
2753
2754    #[test]
2755    fn possible_rsi_invalid_period_rejected() {
2756        let data = sample_close(64);
2757        let err = possible_rsi(&PossibleRsiInput::from_slice(
2758            &data,
2759            PossibleRsiParams {
2760                period: Some(0),
2761                ..PossibleRsiParams::default()
2762            },
2763        ))
2764        .unwrap_err();
2765        assert!(matches!(err, PossibleRsiError::InvalidPeriod { period: 0 }));
2766    }
2767
2768    #[test]
2769    fn possible_rsi_stream_matches_batch() -> Result<(), Box<dyn Error>> {
2770        let data = sample_close(360);
2771        let params = PossibleRsiParams {
2772            period: Some(28),
2773            rsi_mode: Some("cutler".to_string()),
2774            norm_period: Some(90),
2775            normalization_mode: Some("softmax".to_string()),
2776            normalization_length: Some(12),
2777            nonlag_period: Some(11),
2778            dynamic_zone_period: Some(18),
2779            buy_probability: Some(0.2),
2780            sell_probability: Some(0.2),
2781            signal_type: Some("levels_crossover".to_string()),
2782            run_highpass: Some(true),
2783            highpass_period: Some(13),
2784        };
2785        let batch = possible_rsi(&PossibleRsiInput::from_slice(&data, params.clone()))?;
2786        let mut stream = PossibleRsiStream::try_new(params)?;
2787        let mut value = Vec::with_capacity(data.len());
2788        let mut buy_level = Vec::with_capacity(data.len());
2789        let mut sell_level = Vec::with_capacity(data.len());
2790        let mut middle_level = Vec::with_capacity(data.len());
2791        let mut state = Vec::with_capacity(data.len());
2792        let mut long_signal = Vec::with_capacity(data.len());
2793        let mut short_signal = Vec::with_capacity(data.len());
2794        for &sample in &data {
2795            if let Some(point) = stream.update(sample) {
2796                value.push(point.value);
2797                buy_level.push(point.buy_level);
2798                sell_level.push(point.sell_level);
2799                middle_level.push(point.middle_level);
2800                state.push(point.state);
2801                long_signal.push(point.long_signal);
2802                short_signal.push(point.short_signal);
2803            } else {
2804                value.push(f64::NAN);
2805                buy_level.push(f64::NAN);
2806                sell_level.push(f64::NAN);
2807                middle_level.push(f64::NAN);
2808                state.push(f64::NAN);
2809                long_signal.push(0.0);
2810                short_signal.push(0.0);
2811            }
2812        }
2813        let start = state
2814            .iter()
2815            .position(|v| v.is_finite())
2816            .expect("stream should eventually emit finite state");
2817        assert_close(&value[start..], &batch.value[start..], 1e-12);
2818        assert_close(&buy_level[start..], &batch.buy_level[start..], 1e-12);
2819        assert_close(&sell_level[start..], &batch.sell_level[start..], 1e-12);
2820        assert_close(&middle_level[start..], &batch.middle_level[start..], 1e-12);
2821        assert_close(&state[start..], &batch.state[start..], 1e-12);
2822        Ok(())
2823    }
2824
2825    #[test]
2826    fn possible_rsi_dispatch_returns_selected_output() -> Result<(), Box<dyn Error>> {
2827        let data = sample_close(256);
2828        let params = [
2829            ParamKV {
2830                key: "period",
2831                value: ParamValue::Int(28),
2832            },
2833            ParamKV {
2834                key: "rsi_mode",
2835                value: ParamValue::EnumString("regular"),
2836            },
2837            ParamKV {
2838                key: "signal_type",
2839                value: ParamValue::EnumString("zeroline_crossover"),
2840            },
2841        ];
2842        let req = IndicatorComputeRequest {
2843            indicator_id: "possible_rsi",
2844            data: IndicatorDataRef::Slice { values: &data },
2845            params: &params,
2846            output_id: Some("state"),
2847            kernel: Kernel::Auto,
2848        };
2849        let out = compute_cpu(req)?;
2850        let values = match out.series {
2851            IndicatorSeries::F64(values) => values,
2852            other => panic!("unexpected series type: {other:?}"),
2853        };
2854        let direct = possible_rsi(&PossibleRsiInput::from_slice(
2855            &data,
2856            PossibleRsiParams {
2857                period: Some(28),
2858                rsi_mode: Some("regular".to_string()),
2859                signal_type: Some("zeroline_crossover".to_string()),
2860                ..PossibleRsiParams::default()
2861            },
2862        ))?;
2863        assert_close(&values, &direct.state, 1e-12);
2864        Ok(())
2865    }
2866
2867    #[test]
2868    fn possible_rsi_batch_dispatch_returns_selected_output() -> Result<(), Box<dyn Error>> {
2869        let data = sample_close(320);
2870        let params = [
2871            ParamKV {
2872                key: "period",
2873                value: ParamValue::Int(28),
2874            },
2875            ParamKV {
2876                key: "rsi_mode",
2877                value: ParamValue::EnumString("regular"),
2878            },
2879        ];
2880        let combos = [IndicatorParamSet { params: &params }];
2881        let req = IndicatorBatchRequest {
2882            indicator_id: "possible_rsi",
2883            output_id: Some("value"),
2884            combos: &combos,
2885            data: IndicatorDataRef::Slice { values: &data },
2886            kernel: Kernel::Auto,
2887        };
2888        let out = compute_cpu_batch(req)?;
2889        let values = out.values_f64.expect("f64 output");
2890        let direct = possible_rsi(&PossibleRsiInput::from_slice(
2891            &data,
2892            PossibleRsiParams {
2893                period: Some(28),
2894                rsi_mode: Some("regular".to_string()),
2895                ..PossibleRsiParams::default()
2896            },
2897        ))?;
2898        assert_close(&values, &direct.value, 1e-12);
2899        Ok(())
2900    }
2901}
2902
2903#[derive(Debug, Clone)]
2904#[cfg(any())]
2905mod duplicate_impl {
2906    use super::*;
2907
2908    struct HighPassState {
2909        period: usize,
2910        index: usize,
2911        prev_src1: f64,
2912        prev_src2: f64,
2913        prev_hp1: f64,
2914        prev_hp2: f64,
2915    }
2916
2917    impl HighPassState {
2918        #[inline(always)]
2919        fn new(period: usize) -> Self {
2920            Self {
2921                period,
2922                index: 0,
2923                prev_src1: 0.0,
2924                prev_src2: 0.0,
2925                prev_hp1: 0.0,
2926                prev_hp2: 0.0,
2927            }
2928        }
2929
2930        #[inline(always)]
2931        fn reset(&mut self) {
2932            self.index = 0;
2933            self.prev_src1 = 0.0;
2934            self.prev_src2 = 0.0;
2935            self.prev_hp1 = 0.0;
2936            self.prev_hp2 = 0.0;
2937        }
2938
2939        #[inline(always)]
2940        fn update(&mut self, src: f64) -> f64 {
2941            let idx = self.index;
2942            self.index = self.index.saturating_add(1);
2943            if idx < 4 {
2944                self.prev_src2 = self.prev_src1;
2945                self.prev_src1 = src;
2946                self.prev_hp2 = self.prev_hp1;
2947                self.prev_hp1 = 0.0;
2948                return 0.0;
2949            }
2950
2951            let a1 = (-1.414 * std::f64::consts::PI / self.period as f64).exp();
2952            let b1 = 2.0 * a1 * (1.414 * std::f64::consts::PI / self.period as f64).cos();
2953            let c2 = b1;
2954            let c3 = -(a1 * a1);
2955            let c1 = (1.0 + c2 - c3) * 0.25;
2956            let hp = c1 * (src - 2.0 * self.prev_src1 + self.prev_src2)
2957                + c2 * self.prev_hp1
2958                + c3 * self.prev_hp2;
2959            self.prev_src2 = self.prev_src1;
2960            self.prev_src1 = src;
2961            self.prev_hp2 = self.prev_hp1;
2962            self.prev_hp1 = hp;
2963            hp
2964        }
2965    }
2966
2967    #[derive(Debug, Clone)]
2968    struct CutlerRsiState {
2969        period: usize,
2970        prev: Option<f64>,
2971        gains: Vec<f64>,
2972        losses: Vec<f64>,
2973        sum_gain: f64,
2974        sum_loss: f64,
2975        index: usize,
2976        count: usize,
2977    }
2978
2979    impl CutlerRsiState {
2980        #[inline(always)]
2981        fn new(period: usize) -> Self {
2982            Self {
2983                period,
2984                prev: None,
2985                gains: vec![0.0; period],
2986                losses: vec![0.0; period],
2987                sum_gain: 0.0,
2988                sum_loss: 0.0,
2989                index: 0,
2990                count: 0,
2991            }
2992        }
2993
2994        #[inline(always)]
2995        fn reset(&mut self) {
2996            self.prev = None;
2997            self.gains.fill(0.0);
2998            self.losses.fill(0.0);
2999            self.sum_gain = 0.0;
3000            self.sum_loss = 0.0;
3001            self.index = 0;
3002            self.count = 0;
3003        }
3004
3005        #[inline(always)]
3006        fn update(&mut self, value: f64) -> Option<f64> {
3007            let prev = match self.prev {
3008                Some(prev) => prev,
3009                None => {
3010                    self.prev = Some(value);
3011                    return None;
3012                }
3013            };
3014            self.prev = Some(value);
3015            let delta = value - prev;
3016            let gain = delta.max(0.0);
3017            let loss = (-delta).max(0.0);
3018            if self.count == self.period {
3019                self.sum_gain -= self.gains[self.index];
3020                self.sum_loss -= self.losses[self.index];
3021            } else {
3022                self.count += 1;
3023            }
3024            self.gains[self.index] = gain;
3025            self.losses[self.index] = loss;
3026            self.sum_gain += gain;
3027            self.sum_loss += loss;
3028            self.index = (self.index + 1) % self.period;
3029            if self.count < self.period {
3030                return None;
3031            }
3032            let denom = self.sum_gain + self.sum_loss;
3033            Some(if denom.abs() <= f64::EPSILON {
3034                50.0
3035            } else {
3036                100.0 * self.sum_gain / denom
3037            })
3038        }
3039    }
3040
3041    #[derive(Debug, Clone)]
3042    enum PossibleRsiEngine {
3043        Regular(RsiStream),
3044        Rsx(RsxStream),
3045        Cutler(CutlerRsiState),
3046    }
3047
3048    impl PossibleRsiEngine {
3049        #[inline(always)]
3050        fn new(mode: PossibleRsiMode, period: usize) -> Result<Self, PossibleRsiError> {
3051            Ok(match mode {
3052                PossibleRsiMode::Regular => Self::Regular(
3053                    RsiStream::try_new(RsiParams {
3054                        period: Some(period),
3055                    })
3056                    .map_err(|e| PossibleRsiError::InvalidInput { msg: e.to_string() })?,
3057                ),
3058                PossibleRsiMode::Rsx => Self::Rsx(
3059                    RsxStream::try_new(RsxParams {
3060                        period: Some(period),
3061                    })
3062                    .map_err(|e| PossibleRsiError::InvalidInput { msg: e.to_string() })?,
3063                ),
3064                PossibleRsiMode::Cutler => Self::Cutler(CutlerRsiState::new(period)),
3065            })
3066        }
3067
3068        #[inline(always)]
3069        fn update(&mut self, value: f64) -> Option<f64> {
3070            match self {
3071                Self::Regular(inner) => inner.update(value),
3072                Self::Rsx(inner) => inner.update(value),
3073                Self::Cutler(inner) => inner.update(value),
3074            }
3075        }
3076
3077        #[inline(always)]
3078        fn reset(&mut self) {
3079            match self {
3080                Self::Regular(inner) => {
3081                    *inner = RsiStream::try_new(RsiParams {
3082                        period: Some(inner.period),
3083                    })
3084                    .expect("valid RSI params");
3085                }
3086                Self::Rsx(inner) => {
3087                    *inner = RsxStream::try_new(RsxParams {
3088                        period: Some(inner.period),
3089                    })
3090                    .expect("valid RSX params");
3091                }
3092                Self::Cutler(inner) => inner.reset(),
3093            }
3094        }
3095    }
3096
3097    #[derive(Debug, Clone)]
3098    struct RollingWindow {
3099        values: Vec<f64>,
3100        index: usize,
3101        count: usize,
3102    }
3103
3104    impl RollingWindow {
3105        #[inline(always)]
3106        fn new(period: usize) -> Self {
3107            Self {
3108                values: vec![0.0; period.max(1)],
3109                index: 0,
3110                count: 0,
3111            }
3112        }
3113
3114        #[inline(always)]
3115        fn reset(&mut self) {
3116            self.values.fill(0.0);
3117            self.index = 0;
3118            self.count = 0;
3119        }
3120
3121        #[inline(always)]
3122        fn push(&mut self, value: f64) {
3123            self.values[self.index] = value;
3124            self.index = (self.index + 1) % self.values.len();
3125            if self.count < self.values.len() {
3126                self.count += 1;
3127            }
3128        }
3129
3130        #[inline(always)]
3131        fn len(&self) -> usize {
3132            self.values.len()
3133        }
3134
3135        #[inline(always)]
3136        fn is_full(&self) -> bool {
3137            self.count == self.values.len()
3138        }
3139
3140        #[inline(always)]
3141        fn get_recent(&self, age: usize) -> Option<f64> {
3142            if age >= self.count {
3143                return None;
3144            }
3145            let len = self.values.len();
3146            let newest = if self.index == 0 {
3147                len - 1
3148            } else {
3149                self.index - 1
3150            };
3151            let idx = (newest + len - age) % len;
3152            Some(self.values[idx])
3153        }
3154
3155        #[inline(always)]
3156        fn min_max(&self) -> Option<(f64, f64)> {
3157            if !self.is_full() {
3158                return None;
3159            }
3160            let mut min_v = f64::INFINITY;
3161            let mut max_v = f64::NEG_INFINITY;
3162            for &value in &self.values {
3163                if value < min_v {
3164                    min_v = value;
3165                }
3166                if value > max_v {
3167                    max_v = value;
3168                }
3169            }
3170            Some((min_v, max_v))
3171        }
3172
3173        #[inline(always)]
3174        fn mean_std(&self) -> Option<(f64, f64)> {
3175            if !self.is_full() {
3176                return None;
3177            }
3178            let n = self.values.len() as f64;
3179            let mut sum = 0.0;
3180            let mut sumsq = 0.0;
3181            for &value in &self.values {
3182                sum += value;
3183                sumsq += value * value;
3184            }
3185            let mean = sum / n;
3186            let mut var = sumsq / n - mean * mean;
3187            if var < 0.0 {
3188                var = 0.0;
3189            }
3190            Some((mean, var.sqrt()))
3191        }
3192
3193        #[inline(always)]
3194        fn quantile_nearest_rank(&self, q: f64) -> Option<f64> {
3195            if !self.is_full() {
3196                return None;
3197            }
3198            let mut scratch = self.values.clone();
3199            let n = scratch.len();
3200            let rank = ((q * n as f64).ceil() as usize).clamp(1, n);
3201            let idx = rank - 1;
3202            scratch.select_nth_unstable_by(idx, |a, b| a.total_cmp(b));
3203            Some(scratch[idx])
3204        }
3205    }
3206
3207    #[derive(Debug, Clone)]
3208    struct FisherState {
3209        window: RollingWindow,
3210        value_state: f64,
3211        fish_state: f64,
3212    }
3213
3214    impl FisherState {
3215        #[inline(always)]
3216        fn new(period: usize) -> Self {
3217            Self {
3218                window: RollingWindow::new(period),
3219                value_state: 0.0,
3220                fish_state: 0.0,
3221            }
3222        }
3223
3224        #[inline(always)]
3225        fn reset(&mut self) {
3226            self.window.reset();
3227            self.value_state = 0.0;
3228            self.fish_state = 0.0;
3229        }
3230
3231        #[inline(always)]
3232        fn update(&mut self, value: f64) -> Option<f64> {
3233            self.window.push(value);
3234            let (low, high) = self.window.min_max()?;
3235            let range = high - low;
3236            let normalized = if range.abs() <= f64::EPSILON {
3237                0.0
3238            } else {
3239                (value - low) / range - 0.5
3240            };
3241            let mut next = 0.66 * normalized + 0.67 * self.value_state;
3242            next = next.clamp(-0.999, 0.999);
3243            self.value_state = next;
3244            let fish = 0.5 * ((1.0 + next) / (1.0 - next)).ln() + 0.5 * self.fish_state;
3245            self.fish_state = fish;
3246            Some(fish)
3247        }
3248    }
3249
3250    #[derive(Debug, Clone)]
3251    struct NonLagState {
3252        weights: Vec<f64>,
3253        weight_sum: f64,
3254        window: RollingWindow,
3255    }
3256
3257    impl NonLagState {
3258        #[inline(always)]
3259        fn new(period: usize) -> Self {
3260            let cycle = 4.0;
3261            let coeff = 3.0 * std::f64::consts::PI;
3262            let phase = period as f64 - 1.0;
3263            let len = ((period as f64) * cycle + phase) as usize;
3264            let mut weights = vec![0.0; len.max(1)];
3265            let mut weight_sum = 0.0;
3266            for k in 0..weights.len() {
3267                let t = if k as f64 <= phase - 1.0 {
3268                    if phase <= 1.0 {
3269                        0.0
3270                    } else {
3271                        k as f64 / (phase - 1.0)
3272                    }
3273                } else {
3274                    1.0 + (k as f64 - phase + 1.0) * (2.0 * cycle - 1.0)
3275                        / (cycle * period as f64 - 1.0)
3276                };
3277                let beta = (std::f64::consts::PI * t).cos();
3278                let mut g = 1.0 / (coeff * t + 1.0);
3279                if t <= 0.5 {
3280                    g = 1.0;
3281                }
3282                let weight = g * beta;
3283                weights[k] = weight;
3284                weight_sum += weight;
3285            }
3286            Self {
3287                weight_sum,
3288                window: RollingWindow::new(weights.len()),
3289                weights,
3290            }
3291        }
3292
3293        #[inline(always)]
3294        fn reset(&mut self) {
3295            self.window.reset();
3296        }
3297
3298        #[inline(always)]
3299        fn update(&mut self, value: f64) -> f64 {
3300            self.window.push(value);
3301            let mut sum = 0.0;
3302            for k in 0..self.weights.len() {
3303                if let Some(sample) = self.window.get_recent(k) {
3304                    sum += self.weights[k] * sample;
3305                }
3306            }
3307            sum / self.weight_sum
3308        }
3309    }
3310
3311    #[derive(Debug, Clone)]
3312    pub struct PossibleRsiStream {
3313        resolved: PossibleRsiResolved,
3314        highpass: Option<HighPassState>,
3315        engine: PossibleRsiEngine,
3316        norm_window: RollingWindow,
3317        normalization_window: RollingWindow,
3318        fisher: Option<FisherState>,
3319        nonlag: NonLagState,
3320        dz_window: RollingWindow,
3321        prev_value: Option<f64>,
3322        prev_prev_value: Option<f64>,
3323        prev_buy: Option<f64>,
3324        prev_sell: Option<f64>,
3325        prev_middle: Option<f64>,
3326        prev_state: f64,
3327    }
3328
3329    impl PossibleRsiStream {
3330        #[inline(always)]
3331        pub fn try_new(params: PossibleRsiParams) -> Result<Self, PossibleRsiError> {
3332            let resolved = resolve_params(&params)?;
3333            Ok(Self {
3334                highpass: if resolved.run_highpass {
3335                    Some(HighPassState::new(resolved.highpass_period))
3336                } else {
3337                    None
3338                },
3339                engine: PossibleRsiEngine::new(resolved.rsi_mode, resolved.period)?,
3340                norm_window: RollingWindow::new(resolved.norm_period),
3341                normalization_window: RollingWindow::new(resolved.normalization_length),
3342                fisher: if matches!(
3343                    resolved.normalization_mode,
3344                    PossibleRsiNormalizationMode::GaussianFisher
3345                ) {
3346                    Some(FisherState::new(resolved.normalization_length))
3347                } else {
3348                    None
3349                },
3350                nonlag: NonLagState::new(resolved.nonlag_period),
3351                dz_window: RollingWindow::new(resolved.dynamic_zone_period),
3352                prev_value: None,
3353                prev_prev_value: None,
3354                prev_buy: None,
3355                prev_sell: None,
3356                prev_middle: None,
3357                prev_state: 0.0,
3358                resolved,
3359            })
3360        }
3361
3362        #[inline(always)]
3363        pub fn reset(&mut self) {
3364            if let Some(highpass) = &mut self.highpass {
3365                highpass.reset();
3366            }
3367            self.engine.reset();
3368            self.norm_window.reset();
3369            self.normalization_window.reset();
3370            if let Some(fisher) = &mut self.fisher {
3371                fisher.reset();
3372            }
3373            self.nonlag.reset();
3374            self.dz_window.reset();
3375            self.prev_value = None;
3376            self.prev_prev_value = None;
3377            self.prev_buy = None;
3378            self.prev_sell = None;
3379            self.prev_middle = None;
3380            self.prev_state = 0.0;
3381        }
3382
3383        #[inline(always)]
3384        pub fn update(&mut self, value: f64) -> Option<PossibleRsiPoint> {
3385            if !value.is_finite() {
3386                self.reset();
3387                return None;
3388            }
3389            let filtered = if let Some(highpass) = &mut self.highpass {
3390                highpass.update(value)
3391            } else {
3392                value
3393            };
3394            let rsi = self.engine.update(filtered)?;
3395            if !rsi.is_finite() {
3396                self.prev_prev_value = self.prev_value;
3397                self.prev_value = Some(f64::NAN);
3398                return None;
3399            }
3400            self.norm_window.push(rsi);
3401            let (fmin, fmax) = self.norm_window.min_max()?;
3402            let range = fmax - fmin;
3403            if range.abs() <= f64::EPSILON {
3404                self.prev_prev_value = self.prev_value;
3405                self.prev_value = Some(f64::NAN);
3406                return None;
3407            }
3408            let base = 100.0 * (rsi - fmin) / range;
3409            let normalized = match self.resolved.normalization_mode {
3410                PossibleRsiNormalizationMode::GaussianFisher => {
3411                    self.fisher.as_mut().and_then(|state| state.update(base))?
3412                }
3413                PossibleRsiNormalizationMode::Softmax => {
3414                    self.normalization_window.push(base);
3415                    let (mean, stdev) = self.normalization_window.mean_std()?;
3416                    if stdev.abs() <= f64::EPSILON {
3417                        0.0
3418                    } else {
3419                        let z = (base - mean) / stdev;
3420                        (1.0 - (-z).exp()) / (1.0 + (-z).exp())
3421                    }
3422                }
3423                PossibleRsiNormalizationMode::RegularNorm => {
3424                    self.normalization_window.push(base);
3425                    let (mean, stdev) = self.normalization_window.mean_std()?;
3426                    if stdev.abs() <= f64::EPSILON {
3427                        0.0
3428                    } else {
3429                        (base - mean) / (stdev * 3.0)
3430                    }
3431                }
3432            };
3433
3434            let value = self.nonlag.update(normalized);
3435            self.dz_window.push(value);
3436            let buy_level = self
3437                .dz_window
3438                .quantile_nearest_rank(self.resolved.buy_probability)?;
3439            let sell_level = self
3440                .dz_window
3441                .quantile_nearest_rank(1.0 - self.resolved.sell_probability)?;
3442            let middle_level = self.dz_window.quantile_nearest_rank(0.5)?;
3443            let state = match self.resolved.signal_type {
3444                PossibleRsiSignalType::Slope => {
3445                    if let Some(prev) = self.prev_value {
3446                        if value < prev {
3447                            -1.0
3448                        } else if value > prev {
3449                            1.0
3450                        } else {
3451                            self.prev_state
3452                        }
3453                    } else {
3454                        0.0
3455                    }
3456                }
3457                PossibleRsiSignalType::DynamicMiddleCrossover => {
3458                    if value < middle_level {
3459                        -1.0
3460                    } else if value > middle_level {
3461                        1.0
3462                    } else {
3463                        self.prev_state
3464                    }
3465                }
3466                PossibleRsiSignalType::LevelsCrossover => {
3467                    if value < buy_level {
3468                        -1.0
3469                    } else if value > sell_level {
3470                        1.0
3471                    } else {
3472                        self.prev_state
3473                    }
3474                }
3475                PossibleRsiSignalType::ZerolineCrossover => {
3476                    if value < 0.0 {
3477                        -1.0
3478                    } else if value > 0.0 {
3479                        1.0
3480                    } else {
3481                        self.prev_state
3482                    }
3483                }
3484            };
3485            let (long_signal, short_signal) =
3486                self.crossover_signals(value, buy_level, sell_level, middle_level);
3487
3488            self.prev_prev_value = self.prev_value;
3489            self.prev_value = Some(value);
3490            self.prev_buy = Some(buy_level);
3491            self.prev_sell = Some(sell_level);
3492            self.prev_middle = Some(middle_level);
3493            self.prev_state = state;
3494
3495            Some(PossibleRsiPoint {
3496                value,
3497                buy_level,
3498                sell_level,
3499                middle_level,
3500                state,
3501                long_signal,
3502                short_signal,
3503            })
3504        }
3505
3506        #[inline(always)]
3507        fn crossover_signals(
3508            &self,
3509            value: f64,
3510            buy_level: f64,
3511            sell_level: f64,
3512            middle_level: f64,
3513        ) -> (f64, f64) {
3514            let Some(prev_value) = self.prev_value else {
3515                return (0.0, 0.0);
3516            };
3517            match self.resolved.signal_type {
3518                PossibleRsiSignalType::Slope => {
3519                    let Some(prev_prev_value) = self.prev_prev_value else {
3520                        return (0.0, 0.0);
3521                    };
3522                    (
3523                        if prev_value <= prev_prev_value && value > prev_value {
3524                            1.0
3525                        } else {
3526                            0.0
3527                        },
3528                        if prev_value >= prev_prev_value && value < prev_value {
3529                            1.0
3530                        } else {
3531                            0.0
3532                        },
3533                    )
3534                }
3535                PossibleRsiSignalType::DynamicMiddleCrossover => {
3536                    let prev_middle = self.prev_middle.unwrap_or(middle_level);
3537                    (
3538                        if prev_value <= prev_middle && value > middle_level {
3539                            1.0
3540                        } else {
3541                            0.0
3542                        },
3543                        if prev_value >= prev_middle && value < middle_level {
3544                            1.0
3545                        } else {
3546                            0.0
3547                        },
3548                    )
3549                }
3550                PossibleRsiSignalType::LevelsCrossover => {
3551                    let prev_buy = self.prev_buy.unwrap_or(buy_level);
3552                    let prev_sell = self.prev_sell.unwrap_or(sell_level);
3553                    (
3554                        if prev_value <= prev_sell && value > sell_level {
3555                            1.0
3556                        } else {
3557                            0.0
3558                        },
3559                        if prev_value >= prev_buy && value < buy_level {
3560                            1.0
3561                        } else {
3562                            0.0
3563                        },
3564                    )
3565                }
3566                PossibleRsiSignalType::ZerolineCrossover => (
3567                    if prev_value <= 0.0 && value > 0.0 {
3568                        1.0
3569                    } else {
3570                        0.0
3571                    },
3572                    if prev_value >= 0.0 && value < 0.0 {
3573                        1.0
3574                    } else {
3575                        0.0
3576                    },
3577                ),
3578            }
3579        }
3580    }
3581
3582    #[inline(always)]
3583    fn longest_valid_run(data: &[f64]) -> usize {
3584        let mut best = 0usize;
3585        let mut current = 0usize;
3586        for &value in data {
3587            if value.is_finite() {
3588                current += 1;
3589                if current > best {
3590                    best = current;
3591                }
3592            } else {
3593                current = 0;
3594            }
3595        }
3596        best
3597    }
3598
3599    #[inline(always)]
3600    fn resolve_params(params: &PossibleRsiParams) -> Result<PossibleRsiResolved, PossibleRsiError> {
3601        let period = params.period.unwrap_or(32);
3602        if period == 0 {
3603            return Err(PossibleRsiError::InvalidPeriod { period });
3604        }
3605        let norm_period = params.norm_period.unwrap_or(100);
3606        if norm_period == 0 {
3607            return Err(PossibleRsiError::InvalidNormPeriod { norm_period });
3608        }
3609        let normalization_length = params.normalization_length.unwrap_or(15);
3610        if normalization_length == 0 {
3611            return Err(PossibleRsiError::InvalidNormalizationLength {
3612                normalization_length,
3613            });
3614        }
3615        let nonlag_period = params.nonlag_period.unwrap_or(15);
3616        if nonlag_period == 0 {
3617            return Err(PossibleRsiError::InvalidNonlagPeriod { nonlag_period });
3618        }
3619        let dynamic_zone_period = params.dynamic_zone_period.unwrap_or(20);
3620        if dynamic_zone_period == 0 {
3621            return Err(PossibleRsiError::InvalidDynamicZonePeriod {
3622                dynamic_zone_period,
3623            });
3624        }
3625        let highpass_period = params.highpass_period.unwrap_or(15);
3626        if highpass_period == 0 {
3627            return Err(PossibleRsiError::InvalidHighpassPeriod { highpass_period });
3628        }
3629        let buy_probability = params.buy_probability.unwrap_or(0.2);
3630        if !buy_probability.is_finite() || !(0.0..=0.5).contains(&buy_probability) {
3631            return Err(PossibleRsiError::InvalidBuyProbability { buy_probability });
3632        }
3633        let sell_probability = params.sell_probability.unwrap_or(0.2);
3634        if !sell_probability.is_finite() || !(0.0..=0.5).contains(&sell_probability) {
3635            return Err(PossibleRsiError::InvalidSellProbability { sell_probability });
3636        }
3637        Ok(PossibleRsiResolved {
3638            period,
3639            rsi_mode: PossibleRsiMode::from_str(params.rsi_mode.as_deref().unwrap_or("regular"))?,
3640            norm_period,
3641            normalization_mode: PossibleRsiNormalizationMode::from_str(
3642                params
3643                    .normalization_mode
3644                    .as_deref()
3645                    .unwrap_or("gaussian_fisher"),
3646            )?,
3647            normalization_length,
3648            nonlag_period,
3649            dynamic_zone_period,
3650            buy_probability,
3651            sell_probability,
3652            signal_type: PossibleRsiSignalType::from_str(
3653                params
3654                    .signal_type
3655                    .as_deref()
3656                    .unwrap_or("zeroline_crossover"),
3657            )?,
3658            run_highpass: params.run_highpass.unwrap_or(false),
3659            highpass_period,
3660        })
3661    }
3662
3663    #[inline(always)]
3664    fn input_slice<'a>(input: &'a PossibleRsiInput<'a>) -> &'a [f64] {
3665        match &input.data {
3666            PossibleRsiData::Candles { candles, source } => source_type(candles, source),
3667            PossibleRsiData::Slice(data) => data,
3668        }
3669    }
3670
3671    #[inline(always)]
3672    fn validate_common(
3673        data: &[f64],
3674        params: &PossibleRsiParams,
3675    ) -> Result<PossibleRsiResolved, PossibleRsiError> {
3676        if data.is_empty() {
3677            return Err(PossibleRsiError::EmptyInputData);
3678        }
3679        let resolved = resolve_params(params)?;
3680        let valid = longest_valid_run(data);
3681        if valid == 0 {
3682            return Err(PossibleRsiError::AllValuesNaN);
3683        }
3684        let needed = resolved
3685            .period
3686            .saturating_add(resolved.norm_period)
3687            .saturating_add(resolved.normalization_length)
3688            .saturating_add(resolved.dynamic_zone_period);
3689        if valid < needed {
3690            return Err(PossibleRsiError::NotEnoughValidData { needed, valid });
3691        }
3692        Ok(resolved)
3693    }
3694
3695    #[inline(always)]
3696    fn fill_outputs(
3697        data: &[f64],
3698        stream: &mut PossibleRsiStream,
3699        value: &mut [f64],
3700        buy_level: &mut [f64],
3701        sell_level: &mut [f64],
3702        middle_level: &mut [f64],
3703        state: &mut [f64],
3704        long_signal: &mut [f64],
3705        short_signal: &mut [f64],
3706    ) {
3707        for i in 0..data.len() {
3708            match stream.update(data[i]) {
3709                Some(point) => {
3710                    value[i] = point.value;
3711                    buy_level[i] = point.buy_level;
3712                    sell_level[i] = point.sell_level;
3713                    middle_level[i] = point.middle_level;
3714                    state[i] = point.state;
3715                    long_signal[i] = point.long_signal;
3716                    short_signal[i] = point.short_signal;
3717                }
3718                None => {
3719                    value[i] = f64::NAN;
3720                    buy_level[i] = f64::NAN;
3721                    sell_level[i] = f64::NAN;
3722                    middle_level[i] = f64::NAN;
3723                    state[i] = f64::NAN;
3724                    long_signal[i] = f64::NAN;
3725                    short_signal[i] = f64::NAN;
3726                }
3727            }
3728        }
3729    }
3730
3731    #[inline]
3732    pub fn possible_rsi(input: &PossibleRsiInput) -> Result<PossibleRsiOutput, PossibleRsiError> {
3733        possible_rsi_with_kernel(input, Kernel::Auto)
3734    }
3735
3736    pub fn possible_rsi_with_kernel(
3737        input: &PossibleRsiInput,
3738        kernel: Kernel,
3739    ) -> Result<PossibleRsiOutput, PossibleRsiError> {
3740        let data = input_slice(input);
3741        let _resolved = validate_common(data, &input.params)?;
3742        let _chosen = match kernel {
3743            Kernel::Auto => detect_best_kernel(),
3744            other => other,
3745        };
3746        let mut value = alloc_with_nan_prefix(data.len(), 0);
3747        let mut buy_level = alloc_with_nan_prefix(data.len(), 0);
3748        let mut sell_level = alloc_with_nan_prefix(data.len(), 0);
3749        let mut middle_level = alloc_with_nan_prefix(data.len(), 0);
3750        let mut state = alloc_with_nan_prefix(data.len(), 0);
3751        let mut long_signal = alloc_with_nan_prefix(data.len(), 0);
3752        let mut short_signal = alloc_with_nan_prefix(data.len(), 0);
3753        value.fill(f64::NAN);
3754        buy_level.fill(f64::NAN);
3755        sell_level.fill(f64::NAN);
3756        middle_level.fill(f64::NAN);
3757        state.fill(f64::NAN);
3758        long_signal.fill(f64::NAN);
3759        short_signal.fill(f64::NAN);
3760        let mut stream = PossibleRsiStream::try_new(input.params.clone())?;
3761        fill_outputs(
3762            data,
3763            &mut stream,
3764            &mut value,
3765            &mut buy_level,
3766            &mut sell_level,
3767            &mut middle_level,
3768            &mut state,
3769            &mut long_signal,
3770            &mut short_signal,
3771        );
3772        Ok(PossibleRsiOutput {
3773            value,
3774            buy_level,
3775            sell_level,
3776            middle_level,
3777            state,
3778            long_signal,
3779            short_signal,
3780        })
3781    }
3782
3783    pub fn possible_rsi_into_slice(
3784        dst_value: &mut [f64],
3785        dst_buy_level: &mut [f64],
3786        dst_sell_level: &mut [f64],
3787        dst_middle_level: &mut [f64],
3788        dst_state: &mut [f64],
3789        dst_long_signal: &mut [f64],
3790        dst_short_signal: &mut [f64],
3791        input: &PossibleRsiInput,
3792        kernel: Kernel,
3793    ) -> Result<(), PossibleRsiError> {
3794        let data = input_slice(input);
3795        let _resolved = validate_common(data, &input.params)?;
3796        if dst_value.len() != data.len()
3797            || dst_buy_level.len() != data.len()
3798            || dst_sell_level.len() != data.len()
3799            || dst_middle_level.len() != data.len()
3800            || dst_state.len() != data.len()
3801            || dst_long_signal.len() != data.len()
3802            || dst_short_signal.len() != data.len()
3803        {
3804            return Err(PossibleRsiError::OutputLengthMismatch {
3805                expected: data.len(),
3806                got: dst_value.len(),
3807            });
3808        }
3809        let _chosen = match kernel {
3810            Kernel::Auto => detect_best_kernel(),
3811            other => other,
3812        };
3813        let mut stream = PossibleRsiStream::try_new(input.params.clone())?;
3814        fill_outputs(
3815            data,
3816            &mut stream,
3817            dst_value,
3818            dst_buy_level,
3819            dst_sell_level,
3820            dst_middle_level,
3821            dst_state,
3822            dst_long_signal,
3823            dst_short_signal,
3824        );
3825        Ok(())
3826    }
3827
3828    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3829    pub fn possible_rsi_into(
3830        input: &PossibleRsiInput,
3831        dst_value: &mut [f64],
3832        dst_buy_level: &mut [f64],
3833        dst_sell_level: &mut [f64],
3834        dst_middle_level: &mut [f64],
3835        dst_state: &mut [f64],
3836        dst_long_signal: &mut [f64],
3837        dst_short_signal: &mut [f64],
3838    ) -> Result<(), PossibleRsiError> {
3839        possible_rsi_into_slice(
3840            dst_value,
3841            dst_buy_level,
3842            dst_sell_level,
3843            dst_middle_level,
3844            dst_state,
3845            dst_long_signal,
3846            dst_short_signal,
3847            input,
3848            Kernel::Auto,
3849        )
3850    }
3851
3852    #[derive(Debug, Clone, Copy)]
3853    pub struct PossibleRsiBatchRange {
3854        pub period: (usize, usize, usize),
3855        pub norm_period: (usize, usize, usize),
3856        pub normalization_length: (usize, usize, usize),
3857        pub nonlag_period: (usize, usize, usize),
3858        pub dynamic_zone_period: (usize, usize, usize),
3859        pub buy_probability: (f64, f64, f64),
3860        pub sell_probability: (f64, f64, f64),
3861    }
3862
3863    impl Default for PossibleRsiBatchRange {
3864        fn default() -> Self {
3865            Self {
3866                period: (32, 32, 0),
3867                norm_period: (100, 100, 0),
3868                normalization_length: (15, 15, 0),
3869                nonlag_period: (15, 15, 0),
3870                dynamic_zone_period: (20, 20, 0),
3871                buy_probability: (0.2, 0.2, 0.0),
3872                sell_probability: (0.2, 0.2, 0.0),
3873            }
3874        }
3875    }
3876
3877    #[derive(Debug, Clone)]
3878    pub struct PossibleRsiBatchOutput {
3879        pub value: Vec<f64>,
3880        pub buy_level: Vec<f64>,
3881        pub sell_level: Vec<f64>,
3882        pub middle_level: Vec<f64>,
3883        pub state: Vec<f64>,
3884        pub long_signal: Vec<f64>,
3885        pub short_signal: Vec<f64>,
3886        pub combos: Vec<PossibleRsiParams>,
3887        pub rows: usize,
3888        pub cols: usize,
3889    }
3890
3891    #[derive(Debug, Clone, Copy)]
3892    pub struct PossibleRsiBatchBuilder {
3893        range: PossibleRsiBatchRange,
3894        fixed: PossibleRsiParams,
3895        kernel: Kernel,
3896    }
3897
3898    impl Default for PossibleRsiBatchBuilder {
3899        fn default() -> Self {
3900            Self {
3901                range: PossibleRsiBatchRange::default(),
3902                fixed: PossibleRsiParams::default(),
3903                kernel: Kernel::Auto,
3904            }
3905        }
3906    }
3907
3908    impl PossibleRsiBatchBuilder {
3909        #[inline(always)]
3910        pub fn new() -> Self {
3911            Self::default()
3912        }
3913
3914        #[inline(always)]
3915        pub fn kernel(mut self, value: Kernel) -> Self {
3916            self.kernel = value;
3917            self
3918        }
3919
3920        #[inline(always)]
3921        pub fn rsi_mode(mut self, value: &'static str) -> Self {
3922            self.fixed.rsi_mode = Some(value.to_string());
3923            self
3924        }
3925
3926        #[inline(always)]
3927        pub fn normalization_mode(mut self, value: &'static str) -> Self {
3928            self.fixed.normalization_mode = Some(value.to_string());
3929            self
3930        }
3931
3932        #[inline(always)]
3933        pub fn signal_type(mut self, value: &'static str) -> Self {
3934            self.fixed.signal_type = Some(value.to_string());
3935            self
3936        }
3937
3938        #[inline(always)]
3939        pub fn run_highpass(mut self, value: bool) -> Self {
3940            self.fixed.run_highpass = Some(value);
3941            self
3942        }
3943
3944        #[inline(always)]
3945        pub fn highpass_period(mut self, value: usize) -> Self {
3946            self.fixed.highpass_period = Some(value);
3947            self
3948        }
3949
3950        #[inline(always)]
3951        pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
3952            self.range.period = (start, end, step);
3953            self
3954        }
3955
3956        #[inline(always)]
3957        pub fn apply_slice(self, data: &[f64]) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
3958            possible_rsi_batch_with_kernel(data, &self.range, &self.fixed, self.kernel)
3959        }
3960    }
3961
3962    #[inline(always)]
3963    fn expand_axis_usize(
3964        field: &'static str,
3965        range: (usize, usize, usize),
3966    ) -> Result<Vec<usize>, PossibleRsiError> {
3967        let (start, end, step) = range;
3968        if step == 0 || start == end {
3969            return Ok(vec![start]);
3970        }
3971        if start > end {
3972            return Err(PossibleRsiError::InvalidRange {
3973                field,
3974                start: start.to_string(),
3975                end: end.to_string(),
3976                step: step.to_string(),
3977            });
3978        }
3979        let mut values = Vec::new();
3980        let mut current = start;
3981        loop {
3982            values.push(current);
3983            if current >= end {
3984                break;
3985            }
3986            let next = current.saturating_add(step);
3987            if next <= current {
3988                return Err(PossibleRsiError::InvalidRange {
3989                    field,
3990                    start: start.to_string(),
3991                    end: end.to_string(),
3992                    step: step.to_string(),
3993                });
3994            }
3995            current = next.min(end);
3996            if current == *values.last().unwrap() {
3997                break;
3998            }
3999        }
4000        Ok(values)
4001    }
4002
4003    #[inline(always)]
4004    fn expand_axis_f64(
4005        field: &'static str,
4006        range: (f64, f64, f64),
4007    ) -> Result<Vec<f64>, PossibleRsiError> {
4008        let (start, end, step) = range;
4009        if !start.is_finite() || !end.is_finite() || !step.is_finite() {
4010            return Err(PossibleRsiError::InvalidRange {
4011                field,
4012                start: start.to_string(),
4013                end: end.to_string(),
4014                step: step.to_string(),
4015            });
4016        }
4017        if step.abs() <= f64::EPSILON || (start - end).abs() <= f64::EPSILON {
4018            return Ok(vec![start]);
4019        }
4020        if start > end {
4021            return Err(PossibleRsiError::InvalidRange {
4022                field,
4023                start: start.to_string(),
4024                end: end.to_string(),
4025                step: step.to_string(),
4026            });
4027        }
4028        let mut values = Vec::new();
4029        let mut current = start;
4030        while current <= end + 1e-12 {
4031            values.push(current.min(end));
4032            current += step;
4033            if step <= 0.0 {
4034                break;
4035            }
4036        }
4037        Ok(values)
4038    }
4039
4040    fn expand_grid_checked(
4041        range: &PossibleRsiBatchRange,
4042        fixed: &PossibleRsiParams,
4043    ) -> Result<Vec<PossibleRsiParams>, PossibleRsiError> {
4044        let periods = expand_axis_usize("period", range.period)?;
4045        let norm_periods = expand_axis_usize("norm_period", range.norm_period)?;
4046        let normalization_lengths =
4047            expand_axis_usize("normalization_length", range.normalization_length)?;
4048        let nonlag_periods = expand_axis_usize("nonlag_period", range.nonlag_period)?;
4049        let dynamic_zone_periods =
4050            expand_axis_usize("dynamic_zone_period", range.dynamic_zone_period)?;
4051        let buy_probabilities = expand_axis_f64("buy_probability", range.buy_probability)?;
4052        let sell_probabilities = expand_axis_f64("sell_probability", range.sell_probability)?;
4053        let mut out = Vec::new();
4054        for &period in &periods {
4055            for &norm_period in &norm_periods {
4056                for &normalization_length in &normalization_lengths {
4057                    for &nonlag_period in &nonlag_periods {
4058                        for &dynamic_zone_period in &dynamic_zone_periods {
4059                            for &buy_probability in &buy_probabilities {
4060                                for &sell_probability in &sell_probabilities {
4061                                    out.push(PossibleRsiParams {
4062                                        period: Some(period),
4063                                        rsi_mode: fixed.rsi_mode.clone(),
4064                                        norm_period: Some(norm_period),
4065                                        normalization_mode: fixed.normalization_mode.clone(),
4066                                        normalization_length: Some(normalization_length),
4067                                        nonlag_period: Some(nonlag_period),
4068                                        dynamic_zone_period: Some(dynamic_zone_period),
4069                                        buy_probability: Some(buy_probability),
4070                                        sell_probability: Some(sell_probability),
4071                                        signal_type: fixed.signal_type.clone(),
4072                                        run_highpass: fixed.run_highpass,
4073                                        highpass_period: fixed.highpass_period,
4074                                    });
4075                                }
4076                            }
4077                        }
4078                    }
4079                }
4080            }
4081        }
4082        Ok(out)
4083    }
4084
4085    pub fn expand_grid_possible_rsi(
4086        range: &PossibleRsiBatchRange,
4087        fixed: &PossibleRsiParams,
4088    ) -> Vec<PossibleRsiParams> {
4089        expand_grid_checked(range, fixed).unwrap_or_default()
4090    }
4091
4092    pub fn possible_rsi_batch_with_kernel(
4093        data: &[f64],
4094        sweep: &PossibleRsiBatchRange,
4095        fixed: &PossibleRsiParams,
4096        kernel: Kernel,
4097    ) -> Result<PossibleRsiBatchOutput, PossibleRsiError> {
4098        match kernel {
4099            Kernel::Auto
4100            | Kernel::Scalar
4101            | Kernel::ScalarBatch
4102            | Kernel::Avx2
4103            | Kernel::Avx2Batch
4104            | Kernel::Avx512
4105            | Kernel::Avx512Batch => {}
4106            other => return Err(PossibleRsiError::InvalidKernelForBatch(other)),
4107        }
4108        let combos = expand_grid_checked(sweep, fixed)?;
4109        let rows = combos.len();
4110        let cols = data.len();
4111        let total = rows
4112            .checked_mul(cols)
4113            .ok_or_else(|| PossibleRsiError::InvalidInput {
4114                msg: "possible_rsi rows*cols overflow".to_string(),
4115            })?;
4116        let _chosen = match kernel {
4117            Kernel::Auto => detect_best_batch_kernel(),
4118            other => other,
4119        };
4120
4121        let mut value_mu = make_uninit_matrix(rows, cols);
4122        let mut buy_mu = make_uninit_matrix(rows, cols);
4123        let mut sell_mu = make_uninit_matrix(rows, cols);
4124        let mut middle_mu = make_uninit_matrix(rows, cols);
4125        let mut state_mu = make_uninit_matrix(rows, cols);
4126        let mut long_mu = make_uninit_matrix(rows, cols);
4127        let mut short_mu = make_uninit_matrix(rows, cols);
4128        let warmups = vec![0usize; rows];
4129        init_matrix_prefixes(&mut value_mu, cols, &warmups);
4130        init_matrix_prefixes(&mut buy_mu, cols, &warmups);
4131        init_matrix_prefixes(&mut sell_mu, cols, &warmups);
4132        init_matrix_prefixes(&mut middle_mu, cols, &warmups);
4133        init_matrix_prefixes(&mut state_mu, cols, &warmups);
4134        init_matrix_prefixes(&mut long_mu, cols, &warmups);
4135        init_matrix_prefixes(&mut short_mu, cols, &warmups);
4136
4137        let mut value = unsafe {
4138            Vec::from_raw_parts(
4139                value_mu.as_mut_ptr() as *mut f64,
4140                value_mu.len(),
4141                value_mu.capacity(),
4142            )
4143        };
4144        let mut buy_level = unsafe {
4145            Vec::from_raw_parts(
4146                buy_mu.as_mut_ptr() as *mut f64,
4147                buy_mu.len(),
4148                buy_mu.capacity(),
4149            )
4150        };
4151        let mut sell_level = unsafe {
4152            Vec::from_raw_parts(
4153                sell_mu.as_mut_ptr() as *mut f64,
4154                sell_mu.len(),
4155                sell_mu.capacity(),
4156            )
4157        };
4158        let mut middle_level = unsafe {
4159            Vec::from_raw_parts(
4160                middle_mu.as_mut_ptr() as *mut f64,
4161                middle_mu.len(),
4162                middle_mu.capacity(),
4163            )
4164        };
4165        let mut state = unsafe {
4166            Vec::from_raw_parts(
4167                state_mu.as_mut_ptr() as *mut f64,
4168                state_mu.len(),
4169                state_mu.capacity(),
4170            )
4171        };
4172        let mut long_signal = unsafe {
4173            Vec::from_raw_parts(
4174                long_mu.as_mut_ptr() as *mut f64,
4175                long_mu.len(),
4176                long_mu.capacity(),
4177            )
4178        };
4179        let mut short_signal = unsafe {
4180            Vec::from_raw_parts(
4181                short_mu.as_mut_ptr() as *mut f64,
4182                short_mu.len(),
4183                short_mu.capacity(),
4184            )
4185        };
4186        std::mem::forget(value_mu);
4187        std::mem::forget(buy_mu);
4188        std::mem::forget(sell_mu);
4189        std::mem::forget(middle_mu);
4190        std::mem::forget(state_mu);
4191        std::mem::forget(long_mu);
4192        std::mem::forget(short_mu);
4193        debug_assert_eq!(value.len(), total);
4194
4195        let worker = |row: usize,
4196                      dst_value: &mut [f64],
4197                      dst_buy: &mut [f64],
4198                      dst_sell: &mut [f64],
4199                      dst_middle: &mut [f64],
4200                      dst_state: &mut [f64],
4201                      dst_long: &mut [f64],
4202                      dst_short: &mut [f64]| {
4203            dst_value.fill(f64::NAN);
4204            dst_buy.fill(f64::NAN);
4205            dst_sell.fill(f64::NAN);
4206            dst_middle.fill(f64::NAN);
4207            dst_state.fill(f64::NAN);
4208            dst_long.fill(f64::NAN);
4209            dst_short.fill(f64::NAN);
4210            let mut stream = PossibleRsiStream::try_new(combos[row].clone()).expect("valid params");
4211            fill_outputs(
4212                data,
4213                &mut stream,
4214                dst_value,
4215                dst_buy,
4216                dst_sell,
4217                dst_middle,
4218                dst_state,
4219                dst_long,
4220                dst_short,
4221            );
4222        };
4223
4224        #[cfg(not(target_arch = "wasm32"))]
4225        if rows > 1 {
4226            value
4227                .par_chunks_mut(cols)
4228                .zip(buy_level.par_chunks_mut(cols))
4229                .zip(sell_level.par_chunks_mut(cols))
4230                .zip(middle_level.par_chunks_mut(cols))
4231                .zip(state.par_chunks_mut(cols))
4232                .zip(long_signal.par_chunks_mut(cols))
4233                .zip(short_signal.par_chunks_mut(cols))
4234                .enumerate()
4235                .for_each(
4236                    |(
4237                        row,
4238                        (
4239                            (((((dst_value, dst_buy), dst_sell), dst_middle), dst_state), dst_long),
4240                            dst_short,
4241                        ),
4242                    )| {
4243                        worker(
4244                            row, dst_value, dst_buy, dst_sell, dst_middle, dst_state, dst_long,
4245                            dst_short,
4246                        );
4247                    },
4248                );
4249        } else {
4250            for (
4251                row,
4252                (
4253                    (((((dst_value, dst_buy), dst_sell), dst_middle), dst_state), dst_long),
4254                    dst_short,
4255                ),
4256            ) in value
4257                .chunks_mut(cols)
4258                .zip(buy_level.chunks_mut(cols))
4259                .zip(sell_level.chunks_mut(cols))
4260                .zip(middle_level.chunks_mut(cols))
4261                .zip(state.chunks_mut(cols))
4262                .zip(long_signal.chunks_mut(cols))
4263                .zip(short_signal.chunks_mut(cols))
4264                .enumerate()
4265            {
4266                worker(
4267                    row, dst_value, dst_buy, dst_sell, dst_middle, dst_state, dst_long, dst_short,
4268                );
4269            }
4270        }
4271
4272        Ok(PossibleRsiBatchOutput {
4273            value,
4274            buy_level,
4275            sell_level,
4276            middle_level,
4277            state,
4278            long_signal,
4279            short_signal,
4280            combos,
4281            rows,
4282            cols,
4283        })
4284    }
4285}