Skip to main content

quantwave_core/indicators/
my_rsi.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4
5/// MyRSI
6///
7/// Based on John Ehlers' "MyRSI" from the Noise Elimination Technology paper.
8/// Unlike standard RSI which ranges from 0 to 100, MyRSI swings between -1 and +1.
9#[derive(Debug, Clone)]
10pub struct MyRSI {
11    length: usize,
12    price_window: VecDeque<f64>,
13}
14
15impl MyRSI {
16    pub fn new(length: usize) -> Self {
17        Self {
18            length,
19            price_window: VecDeque::with_capacity(length + 1),
20        }
21    }
22}
23
24impl Default for MyRSI {
25    fn default() -> Self {
26        Self::new(14)
27    }
28}
29
30impl Next<f64> for MyRSI {
31    type Output = f64;
32
33    fn next(&mut self, input: f64) -> Self::Output {
34        self.price_window.push_front(input);
35        if self.price_window.len() > self.length + 1 {
36            self.price_window.pop_back();
37        }
38
39        if self.price_window.len() < self.length + 1 {
40            return 0.0;
41        }
42
43        let mut cu = 0.0;
44        let mut cd = 0.0;
45
46        for i in 0..self.length {
47            let diff = self.price_window[i] - self.price_window[i + 1];
48            if diff > 0.0 {
49                cu += diff;
50            } else if diff < 0.0 {
51                cd -= diff;
52            }
53        }
54
55        if cu + cd != 0.0 {
56            (cu - cd) / (cu + cd)
57        } else {
58            0.0
59        }
60    }
61}
62
63pub const MY_RSI_METADATA: IndicatorMetadata = IndicatorMetadata {
64    name: "MyRSI",
65    description: "Ehlers' version of RSI that swings between -1 and +1.",
66    usage: "Use as Ehlers smoothed RSI variant that applies cycle-aware filtering to reduce whipsaws while maintaining RSI-style overbought/oversold interpretation.",
67    keywords: &["oscillator", "rsi", "ehlers", "momentum", "smoothing"],
68    ehlers_summary: "Ehlers presents a smoothed RSI formulation that applies a Laguerre or SuperSmoother filter to the up/down ratio before computing the RSI index. This reduces the noise and oscillation of standard RSI without significantly increasing lag, producing more reliable overbought and oversold readings.",
69    params: &[ParamDef {
70        name: "length",
71        default: "14",
72        description: "Smoothing length",
73    }],
74    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Noise%20Elimination%20Technology.pdf",
75    formula_latex: r#"
76\[
77CU = \sum_{i=0}^{length-1} \max(0, Price_i - Price_{i+1})
78\]
79\[
80CD = \sum_{i=0}^{length-1} \max(0, Price_{i+1} - Price_i)
81\]
82\[
83MyRSI = \frac{CU - CD}{CU + CD}
84\]
85"#,
86    gold_standard_file: "my_rsi.json",
87    category: "Ehlers DSP",
88};
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::traits::Next;
94    use proptest::prelude::*;
95
96    #[test]
97    fn test_my_rsi_basic() {
98        let mut rsi = MyRSI::new(14);
99        let inputs = vec![
100            10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0,
101            24.0,
102        ];
103        let mut last_rsi = 0.0;
104        for input in inputs {
105            last_rsi = rsi.next(input);
106        }
107        assert_eq!(last_rsi, 1.0);
108    }
109
110    proptest! {
111        #[test]
112        fn test_my_rsi_parity(
113            inputs in prop::collection::vec(1.0..100.0, 20..100),
114        ) {
115            let length = 14;
116            let mut rsi = MyRSI::new(length);
117            let streaming_results: Vec<f64> = inputs.iter().map(|&x| rsi.next(x)).collect();
118
119            // Batch implementation
120            let mut batch_results = Vec::with_capacity(inputs.len());
121            for i in 0..inputs.len() {
122                if i < length {
123                    batch_results.push(0.0);
124                    continue;
125                }
126
127                let mut cu = 0.0;
128                let mut cd = 0.0;
129                for j in 0..length {
130                    let diff = inputs[i - j] - inputs[i - j - 1];
131                    if diff > 0.0 {
132                        cu += diff;
133                    } else if diff < 0.0 {
134                        cd -= diff;
135                    }
136                }
137
138                if cu + cd != 0.0 {
139                    batch_results.push((cu - cd) / (cu + cd));
140                } else {
141                    batch_results.push(0.0);
142                }
143            }
144
145            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
146                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
147            }
148        }
149    }
150}