quantwave_core/indicators/
rsih.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as VecDeque;
4use std::f64::consts::PI;
5
6#[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 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}