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 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 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}