Skip to main content

quantwave_core/indicators/
rocket_rsi.rs

1use 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/// RocketRSI
8///
9/// Based on John Ehlers' "RocketRSI" (TASC May 2018).
10/// It applies a SuperSmoother filter to momentum, then computes an RSI variant (MyRSI)
11/// and finally applies a Fisher Transform.
12#[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        // 1. Momentum
53        let mom = self.price_window[0] - self.price_window[self.rsi_length - 1];
54
55        // 2. Filtered Momentum
56        let filt = self.smoother.next(mom);
57
58        // 3. MyRSI on Filtered Momentum
59        let my_rsi_val = self.my_rsi.next(filt);
60
61        // 4. Fisher Transform
62        // Fisher = 0.5 * Log((1 + MyRSI) / (1 - MyRSI))
63        // We need to clamp MyRSI to avoid log(0) or log(negative)
64        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            // Batch implementation
133            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}