wickra_embed_core/indicators/
rsi.rs1use crate::math;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
27pub struct Rsi<const N: usize> {
28 n_minus_1: f64,
29 inv_period: f64,
30 prev_close: f64,
31 has_prev: bool,
32 seed_count: usize,
36 seed_sum_gain: f64,
37 seed_sum_loss: f64,
38 avg_gain: f64,
39 avg_loss: f64,
40 avgs_seeded: bool,
41 last_value: Option<f64>,
42}
43
44impl<const N: usize> Rsi<N> {
45 #[must_use]
47 pub fn new() -> Self {
48 Self {
49 n_minus_1: (N - 1) as f64,
50 inv_period: 1.0 / N as f64,
51 prev_close: 0.0,
52 has_prev: false,
53 seed_count: 0,
54 seed_sum_gain: 0.0,
55 seed_sum_loss: 0.0,
56 avg_gain: 0.0,
57 avg_loss: 0.0,
58 avgs_seeded: false,
59 last_value: None,
60 }
61 }
62
63 #[must_use]
65 pub const fn value(&self) -> Option<f64> {
66 self.last_value
67 }
68
69 fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
72 let denom = avg_gain + avg_loss;
73 if denom == 0.0 {
74 50.0
75 } else {
76 100.0 * avg_gain / denom
77 }
78 }
79}
80
81impl<const N: usize> Default for Rsi<N> {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl<const N: usize> Indicator for Rsi<N> {
88 type Input = f64;
89
90 fn update(&mut self, input: f64) -> Option<f64> {
91 if !input.is_finite() {
92 return self.last_value;
93 }
94 if !self.has_prev {
95 self.prev_close = input;
96 self.has_prev = true;
97 return None;
98 }
99 let prev = self.prev_close;
100 self.prev_close = input;
101
102 let diff = input - prev;
103 let gain = if diff > 0.0 { diff } else { 0.0 };
104 let loss = if diff < 0.0 { -diff } else { 0.0 };
105
106 if self.avgs_seeded {
107 let smoothed_gain =
108 math::mul_add(self.avg_gain, self.n_minus_1, gain) * self.inv_period;
109 let smoothed_loss =
110 math::mul_add(self.avg_loss, self.n_minus_1, loss) * self.inv_period;
111 self.avg_gain = smoothed_gain;
112 self.avg_loss = smoothed_loss;
113 let v = Self::rsi_from_avgs(smoothed_gain, smoothed_loss);
114 self.last_value = Some(v);
115 return Some(v);
116 }
117
118 self.seed_sum_gain += gain;
119 self.seed_sum_loss += loss;
120 self.seed_count += 1;
121 if self.seed_count == N {
122 let ag = self.seed_sum_gain / N as f64;
123 let al = self.seed_sum_loss / N as f64;
124 self.avg_gain = ag;
125 self.avg_loss = al;
126 self.avgs_seeded = true;
127 let v = Self::rsi_from_avgs(ag, al);
128 self.last_value = Some(v);
129 return Some(v);
130 }
131 None
132 }
133
134 fn reset(&mut self) {
135 *self = Self::new();
136 }
137
138 fn warmup_period(&self) -> usize {
139 N + 1
140 }
141
142 fn is_ready(&self) -> bool {
143 self.last_value.is_some()
144 }
145
146 fn name(&self) -> &'static str {
147 "rsi"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::Rsi;
154 use crate::traits::Indicator;
155
156 #[test]
157 fn warmup_and_bounds() {
158 let mut rsi = Rsi::<3>::new();
159 assert_eq!(rsi.update(100.0), None);
161 assert_eq!(rsi.warmup_period(), 4);
162 let mut last = None;
163 for i in 1..40 {
164 last = rsi.update(100.0 + f64::from(i % 7));
165 }
166 let v = last.unwrap();
167 assert!((0.0..=100.0).contains(&v), "rsi out of range: {v}");
168 assert!(rsi.is_ready());
169 }
170
171 #[test]
172 fn all_gains_saturates_to_100() {
173 let mut rsi = Rsi::<3>::new();
174 let mut last = None;
175 for i in 0..20 {
176 last = rsi.update(100.0 + f64::from(i));
177 }
178 assert_eq!(last, Some(100.0));
179 }
180
181 #[test]
182 fn byte_parity_with_wickra_core() {
183 let mut ours = Rsi::<14>::new();
184 let mut theirs = wickra_core::Rsi::new(14).unwrap();
185 for i in 0..1000 {
186 let x = 100.0 + 20.0 * (f64::from(i) * 0.09).sin() + 5.0 * (f64::from(i) * 0.3).cos();
187 let a = ours.update(x);
188 let b = <wickra_core::Rsi as wickra_core::Indicator>::update(&mut theirs, x);
189 assert_eq!(a.map(f64::to_bits), b.map(f64::to_bits), "index {i}");
190 }
191 }
192}