quantwave_core/indicators/
rocket_rsi.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::my_rsi::MyRSI;
3use crate::indicators::super_smoother::SuperSmoother;
4use crate::traits::Next;
5use std::collections::VecDeque;
6
7#[derive(Debug, Clone)]
13pub struct RocketRSI {
14 rsi_length: usize,
15 _smooth_length: usize,
16 price_window: VecDeque<f64>,
17 smoother: SuperSmoother,
18 my_rsi: MyRSI,
19}
20
21impl RocketRSI {
22 pub fn new(rsi_length: usize, smooth_length: usize) -> Self {
23 Self {
24 rsi_length,
25 _smooth_length: smooth_length,
26 price_window: VecDeque::with_capacity(rsi_length),
27 smoother: SuperSmoother::new(smooth_length),
28 my_rsi: MyRSI::new(rsi_length),
29 }
30 }
31}
32
33impl Default for RocketRSI {
34 fn default() -> Self {
35 Self::new(8, 10)
36 }
37}
38
39impl Next<f64> for RocketRSI {
40 type Output = f64;
41
42 fn next(&mut self, input: f64) -> Self::Output {
43 self.price_window.push_front(input);
44 if self.price_window.len() > self.rsi_length {
45 self.price_window.pop_back();
46 }
47
48 if self.price_window.len() < self.rsi_length {
49 return 0.0;
50 }
51
52 let mom = self.price_window[0] - self.price_window[self.rsi_length - 1];
54
55 let filt = self.smoother.next(mom);
57
58 let my_rsi_val = self.my_rsi.next(filt);
60
61 let clamped_rsi = my_rsi_val.clamp(-0.999, 0.999);
65 0.5 * ((1.0 + clamped_rsi) / (1.0 - clamped_rsi)).ln()
66 }
67}
68
69pub const ROCKET_RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
70 name: "RocketRSI",
71 description: "Highly responsive RSI variant using SuperSmoother and Fisher Transform.",
72 usage: "Use for rapid cycle identification and reversal detection. The Fisher Transform converts the RSI distribution into a Gaussian-like distribution with sharp peaks at reversals.",
73 keywords: &["oscillator", "rsi", "ehlers", "dsp", "fisher", "momentum"],
74 ehlers_summary: "RocketRSI improves upon standard RSI by first smoothing the momentum with a SuperSmoother filter to eliminate high-frequency noise. The resulting RSI is then passed through a Fisher Transform to create clear, actionable signals at cyclical turning points.",
75 params: &[
76 ParamDef {
77 name: "rsi_length",
78 default: "8",
79 description: "RSI calculation period",
80 },
81 ParamDef {
82 name: "smooth_length",
83 default: "10",
84 description: "SuperSmoother filter period",
85 },
86 ],
87 formula_source: "https://www.traders.com/Documentation/FEEDbk_docs/2018/05/TradersTips.html",
88 formula_latex: r#"
89\[
90Mom = Price - Price_{t-(L-1)}
91\]
92\[
93Filt = \text{SuperSmoother}(Mom, SL)
94\]
95\[
96MyRSI = \frac{\sum \max(0, \Delta Filt) - \sum \max(0, -\Delta Filt)}{\sum |\Delta Filt|}
97\]
98\[
99RocketRSI = 0.5 \cdot \ln\left(\frac{1 + MyRSI}{1 - MyRSI}\right)
100\]
101"#,
102 gold_standard_file: "rocket_rsi.json",
103 category: "Ehlers DSP",
104};
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::Next;
110 use proptest::proptest;
111 use proptest::prelude::*;
112
113 #[test]
114 fn test_rocket_rsi_basic() {
115 let mut rocket = RocketRSI::new(8, 10);
116 for i in 0..100 {
117 let input = (i as f64 * 0.1).sin() * 100.0;
118 let _ = rocket.next(input);
119 }
120 }
121
122 proptest! {
123 #[test]
124 fn test_rocket_rsi_parity(
125 inputs in prop::collection::vec(1.0..100.0, 50..100),
126 ) {
127 let rsi_length = 8;
128 let smooth_length = 10;
129 let mut rocket = RocketRSI::new(rsi_length, smooth_length);
130 let streaming_results: Vec<f64> = inputs.iter().map(|&x| rocket.next(x)).collect();
131
132 let mut batch_results = Vec::with_capacity(inputs.len());
134
135 let mut price_window = VecDeque::with_capacity(rsi_length);
136 let mut smoother = crate::indicators::super_smoother::SuperSmoother::new(smooth_length);
137 let mut my_rsi = crate::indicators::my_rsi::MyRSI::new(rsi_length);
138
139 for i in 0..inputs.len() {
140 price_window.push_front(inputs[i]);
141 if price_window.len() > rsi_length {
142 price_window.pop_back();
143 }
144
145 if price_window.len() < rsi_length {
146 batch_results.push(0.0);
147 continue;
148 }
149
150 let mom = price_window[0] - price_window[rsi_length - 1];
151 let filt = smoother.next(mom);
152 let rsi_val = my_rsi.next(filt);
153 let clamped = rsi_val.clamp(-0.999, 0.999);
154 batch_results.push(0.5 * ((1.0 + clamped) / (1.0 - clamped)).ln());
155 }
156
157 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
158 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
159 }
160 }
161 }
162}