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