Skip to main content

quantwave_core/indicators/
rsih.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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: &[ParamDef {
80        name: "length",
81        default: "14",
82        description: "RSI length",
83    }],
84    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JANUARY%202022.html",
85    formula_latex: r#"
86\[
87CU = \sum_{n=1}^L (1 - \cos\left(\frac{2\pi n}{L+1}\right)) \cdot \max(0, Close_{t-n+1} - Close_{t-n})
88\]
89\[
90CD = \sum_{n=1}^L (1 - \cos\left(\frac{2\pi n}{L+1}\right)) \cdot \max(0, Close_{t-n} - Close_{t-n+1})
91\]
92\[
93RSIH = \frac{CU - CD}{CU + CD}
94\]
95"#,
96    gold_standard_file: "rsih.json",
97    category: "Ehlers DSP",
98};
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::test_utils::{assert_indicator_parity, load_gold_standard};
104    use crate::traits::Next;
105    use proptest::prelude::*;
106
107    #[test]
108    fn test_rsih_gold_standard() {
109        let case = load_gold_standard("rsih");
110        let rsih = RSIH::new(14);
111        assert_indicator_parity(rsih, &case.input, &case.expected);
112    }
113
114    #[test]
115    fn test_rsih_basic() {
116        let mut rsih = RSIH::default();
117        let inputs = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
118        for input in inputs {
119            let res = rsih.next(input);
120            assert!(!res.is_nan());
121        }
122    }
123
124    proptest! {
125        #[test]
126        fn test_rsih_parity(
127            inputs in prop::collection::vec(1.0..100.0, 50..100),
128        ) {
129            let length = 14;
130            let mut rsih = RSIH::new(length);
131            let streaming_results: Vec<f64> = inputs.iter().map(|&x| rsih.next(x)).collect();
132
133            // Batch implementation
134            let mut batch_results = Vec::with_capacity(inputs.len());
135            let mut coeffs = Vec::new();
136            for count in 1..=length {
137                let c = 1.0 - (2.0 * PI * count as f64 / (length as f64 + 1.0)).cos();
138                coeffs.push(c);
139            }
140
141            for i in 0..inputs.len() {
142                if i < length {
143                    batch_results.push(0.0);
144                    continue;
145                }
146                let mut cu = 0.0;
147                let mut cd = 0.0;
148                for count in 1..=length {
149                    let change = inputs[i - count + 1] - inputs[i - count];
150                    let coef = coeffs[count - 1];
151                    if change > 0.0 {
152                        cu += coef * change;
153                    } else if change < 0.0 {
154                        cd += coef * change.abs();
155                    }
156                }
157                let res = if (cu + cd).abs() > 1e-10 {
158                    (cu - cd) / (cu + cd)
159                } else {
160                    0.0
161                };
162                batch_results.push(res);
163            }
164
165            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
166                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
167            }
168        }
169    }
170}