quantwave_core/indicators/
rsih.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::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: &[
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 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}