Skip to main content

quantwave_core/indicators/
rsih.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6/// RSI with Hann Windowing (RSIH)
7///
8/// Based on John Ehlers' "(Yet Another) Improved RSI" (January 2022).
9/// It applies Hann window coefficients to price changes to create a smoother, 
10/// zero-centered oscillator.
11#[derive(Debug, Clone)]
12pub struct RSIH {
13    length: usize,
14    price_history: VecDeque<f64>,
15    coefficients: Vec<f64>,
16}
17
18impl RSIH {
19    pub fn new(length: usize) -> Self {
20        let mut coefficients = Vec::with_capacity(length);
21        for count in 1..=length {
22            let coef = 1.0 - (2.0 * PI * count as f64 / (length as f64 + 1.0)).cos();
23            coefficients.push(coef);
24        }
25        Self {
26            length,
27            price_history: VecDeque::with_capacity(length + 1),
28            coefficients,
29        }
30    }
31}
32
33impl Default for RSIH {
34    fn default() -> Self {
35        Self::new(14)
36    }
37}
38
39impl Next<f64> for RSIH {
40    type Output = f64;
41
42    fn next(&mut self, input: f64) -> Self::Output {
43        self.price_history.push_front(input);
44        if self.price_history.len() > self.length + 1 {
45            self.price_history.pop_back();
46        }
47
48        if self.price_history.len() < self.length + 1 {
49            return 0.0;
50        }
51
52        let mut cu = 0.0;
53        let mut cd = 0.0;
54
55        for count in 1..=self.length {
56            let change = self.price_history[count - 1] - self.price_history[count];
57            let coef = self.coefficients[count - 1];
58            if change > 0.0 {
59                cu += coef * change;
60            } else if change < 0.0 {
61                cd += coef * change.abs();
62            }
63        }
64
65        if (cu + cd).abs() > 1e-10 {
66            (cu - cd) / (cu + cd)
67        } else {
68            0.0
69        }
70    }
71}
72
73pub const RSIH_METADATA: IndicatorMetadata = IndicatorMetadata {
74    name: "RSIH",
75    description: "RSI enhanced with Hann windowing for superior smoothing and zero-centering.",
76    usage: "Use to measure momentum exclusively on the cyclical (high-pass filtered) component of price, eliminating the trend bias that makes standard RSI drift.",
77    keywords: &["oscillator", "rsi", "ehlers", "high-pass", "cycle"],
78    ehlers_summary: "RSIH applies RSI computation to the high-pass filtered price rather than raw price. By removing the trend component first, the RSI calculation operates only on the cyclical content of the market, producing an oscillator that is centered around zero regardless of the prevailing trend direction.",
79    params: &[
80        ParamDef { name: "length", default: "14", description: "RSI length" },
81    ],
82    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JANUARY%202022.html",
83    formula_latex: r#"
84\[
85CU = \sum_{n=1}^L (1 - \cos\left(\frac{2\pi n}{L+1}\right)) \cdot \max(0, Close_{t-n+1} - Close_{t-n})
86\]
87\[
88CD = \sum_{n=1}^L (1 - \cos\left(\frac{2\pi n}{L+1}\right)) \cdot \max(0, Close_{t-n} - Close_{t-n+1})
89\]
90\[
91RSIH = \frac{CU - CD}{CU + CD}
92\]
93"#,
94    gold_standard_file: "rsih.json",
95    category: "Ehlers DSP",
96};
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::traits::Next;
102    use crate::test_utils::{load_gold_standard, assert_indicator_parity};
103    use proptest::prelude::*;
104
105    #[test]
106    fn test_rsih_gold_standard() {
107        let case = load_gold_standard("rsih");
108        let rsih = RSIH::new(14);
109        assert_indicator_parity(rsih, &case.input, &case.expected);
110    }
111
112    #[test]
113    fn test_rsih_basic() {
114        let mut rsih = RSIH::default();
115        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
116        for input in inputs {
117            let res = rsih.next(input);
118            assert!(!res.is_nan());
119        }
120    }
121
122    proptest! {
123        #[test]
124        fn test_rsih_parity(
125            inputs in prop::collection::vec(1.0..100.0, 50..100),
126        ) {
127            let length = 14;
128            let mut rsih = RSIH::new(length);
129            let streaming_results: Vec<f64> = inputs.iter().map(|&x| rsih.next(x)).collect();
130
131            // Batch implementation
132            let mut batch_results = Vec::with_capacity(inputs.len());
133            let mut coeffs = Vec::new();
134            for count in 1..=length {
135                let c = 1.0 - (2.0 * PI * count as f64 / (length as f64 + 1.0)).cos();
136                coeffs.push(c);
137            }
138
139            for i in 0..inputs.len() {
140                if i < length {
141                    batch_results.push(0.0);
142                    continue;
143                }
144                let mut cu = 0.0;
145                let mut cd = 0.0;
146                for count in 1..=length {
147                    let change = inputs[i - count + 1] - inputs[i - count];
148                    let coef = coeffs[count - 1];
149                    if change > 0.0 {
150                        cu += coef * change;
151                    } else if change < 0.0 {
152                        cd += coef * change.abs();
153                    }
154                }
155                let res = if (cu + cd).abs() > 1e-10 {
156                    (cu - cd) / (cu + cd)
157                } else {
158                    0.0
159                };
160                batch_results.push(res);
161            }
162
163            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
164                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
165            }
166        }
167    }
168}