Skip to main content

quantwave_core/indicators/
my_rsi.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::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    params: &[ParamDef {
67        name: "length",
68        default: "14",
69        description: "Smoothing length",
70    }],
71    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Noise%20Elimination%20Technology.pdf",
72    formula_latex: r#"
73\[
74CU = \sum_{i=0}^{length-1} \max(0, Price_i - Price_{i+1})
75\]
76\[
77CD = \sum_{i=0}^{length-1} \max(0, Price_{i+1} - Price_i)
78\]
79\[
80MyRSI = \frac{CU - CD}{CU + CD}
81\]
82"#,
83    gold_standard_file: "my_rsi.json",
84    category: "Ehlers DSP",
85};
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::traits::Next;
91    use proptest::prelude::*;
92
93    #[test]
94    fn test_my_rsi_basic() {
95        let mut rsi = MyRSI::new(14);
96        let inputs = vec![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, 24.0];
97        let mut last_rsi = 0.0;
98        for input in inputs {
99            last_rsi = rsi.next(input);
100        }
101        assert_eq!(last_rsi, 1.0);
102    }
103
104    proptest! {
105        #[test]
106        fn test_my_rsi_parity(
107            inputs in prop::collection::vec(1.0..100.0, 20..100),
108        ) {
109            let length = 14;
110            let mut rsi = MyRSI::new(length);
111            let streaming_results: Vec<f64> = inputs.iter().map(|&x| rsi.next(x)).collect();
112
113            // Batch implementation
114            let mut batch_results = Vec::with_capacity(inputs.len());
115            for i in 0..inputs.len() {
116                if i < length {
117                    batch_results.push(0.0);
118                    continue;
119                }
120
121                let mut cu = 0.0;
122                let mut cd = 0.0;
123                for j in 0..length {
124                    let diff = inputs[i - j] - inputs[i - j - 1];
125                    if diff > 0.0 {
126                        cu += diff;
127                    } else if diff < 0.0 {
128                        cd -= diff;
129                    }
130                }
131
132                if cu + cd != 0.0 {
133                    batch_results.push((cu - cd) / (cu + cd));
134                } else {
135                    batch_results.push(0.0);
136                }
137            }
138
139            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
140                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
141            }
142        }
143    }
144}