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(¶ms)?;
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: ¶ms,
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: ¶ms }];
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(¶ms)?;
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}