finance_solution/stocks/ta/
rsi.rs1use crate::stocks::ta::common::{opt_cell, validate_series};
46use crate::util::error::FinanceResult;
47use crate::util::primitives::PeriodLength;
48use crate::{columns_with_strings, print_table_locale_opt};
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52pub struct RsiParams {
53 pub period: usize,
54}
55
56impl RsiParams {
57 pub const fn new(period: usize) -> Self {
58 Self { period }
59 }
60
61 pub const fn period_14() -> Self {
63 Self { period: 14 }
64 }
65
66 pub const fn period_7() -> Self {
67 Self { period: 7 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73pub struct ValidatedRsi {
74 params: RsiParams,
75}
76
77impl ValidatedRsi {
78 pub fn new(params: RsiParams) -> FinanceResult<Self> {
79 PeriodLength::new(params.period)?;
80 Ok(Self { params })
81 }
82
83 pub fn params(self) -> RsiParams {
84 self.params
85 }
86
87 pub fn compute(self, closes: &[f64]) -> FinanceResult<RsiSeries> {
88 rsi_validated(closes, self)
89 }
90}
91
92#[derive(Clone, Debug, PartialEq)]
93pub struct RsiSeries {
94 pub rsi: Vec<Option<f64>>,
95 pub params: RsiParams,
96}
97
98impl RsiSeries {
99 pub fn last(&self) -> Option<f64> {
100 self.rsi.iter().rev().find_map(|x| *x)
101 }
102}
103
104#[derive(Clone, Debug)]
105pub struct RsiSolution {
106 series: RsiSeries,
107 closes: Vec<f64>,
108 formula: String,
109 symbolic_formula: String,
110}
111
112impl RsiSolution {
113 pub fn series(&self) -> &RsiSeries {
114 &self.series
115 }
116 pub fn formula(&self) -> &str {
117 &self.formula
118 }
119 pub fn symbolic_formula(&self) -> &str {
120 &self.symbolic_formula
121 }
122
123 pub fn print_table(&self) {
124 self.print_table_locale_opt(None, None);
125 }
126
127 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
128 self.print_table_locale_opt(Some(locale), Some(precision));
129 }
130
131 fn print_table_locale_opt(
132 &self,
133 locale: Option<&num_format::Locale>,
134 precision: Option<usize>,
135 ) {
136 let columns = columns_with_strings(&[
137 ("period", "i", true),
138 ("close", "f", true),
139 ("rsi", "f", true),
140 ]);
141 let data = self
142 .closes
143 .iter()
144 .enumerate()
145 .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.rsi[i])])
146 .collect();
147 print_table_locale_opt(&columns, data, locale, precision);
148 }
149}
150
151#[derive(Clone, Debug, PartialEq)]
153pub struct RsiState {
154 params: RsiParams,
155 prev_close: Option<f64>,
156 avg_gain: Option<f64>,
157 avg_loss: Option<f64>,
158 seed_gains: Vec<f64>,
160 seed_losses: Vec<f64>,
161 last: Option<f64>,
162 bars: usize,
163}
164
165impl RsiState {
166 pub fn new(params: RsiParams) -> FinanceResult<Self> {
167 PeriodLength::new(params.period)?;
168 Ok(Self {
169 params,
170 prev_close: None,
171 avg_gain: None,
172 avg_loss: None,
173 seed_gains: Vec::with_capacity(params.period),
174 seed_losses: Vec::with_capacity(params.period),
175 last: None,
176 bars: 0,
177 })
178 }
179
180 pub fn from_history(params: RsiParams, closes: &[f64]) -> FinanceResult<Self> {
181 let mut s = Self::new(params)?;
182 let _ = s.push_bars(closes)?;
183 Ok(s)
184 }
185
186 pub fn push(&mut self, close: f64) -> FinanceResult<Option<f64>> {
187 crate::util::error::require_finite("close", close)?;
188 self.bars += 1;
189 let period = self.params.period;
190 let out = if let Some(prev) = self.prev_close {
191 let ch = close - prev;
192 let gain = ch.max(0.0);
193 let loss = (-ch).max(0.0);
194 if self.avg_gain.is_none() {
195 self.seed_gains.push(gain);
196 self.seed_losses.push(loss);
197 if self.seed_gains.len() == period {
198 let ag = self.seed_gains.iter().sum::<f64>() / period as f64;
199 let al = self.seed_losses.iter().sum::<f64>() / period as f64;
200 self.avg_gain = Some(ag);
201 self.avg_loss = Some(al);
202 self.last = Some(rsi_from_avgs(ag, al));
203 self.last
204 } else {
205 None
206 }
207 } else {
208 let ag = self.avg_gain.unwrap();
209 let al = self.avg_loss.unwrap();
210 let ag = (ag * (period as f64 - 1.0) + gain) / period as f64;
211 let al = (al * (period as f64 - 1.0) + loss) / period as f64;
212 self.avg_gain = Some(ag);
213 self.avg_loss = Some(al);
214 self.last = Some(rsi_from_avgs(ag, al));
215 self.last
216 }
217 } else {
218 None
219 };
220 self.prev_close = Some(close);
221 Ok(out)
222 }
223
224 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
225 let mut out = Vec::with_capacity(closes.len());
226 for &c in closes {
227 out.push(self.push(c)?);
228 }
229 Ok(out)
230 }
231
232 pub fn last(&self) -> Option<f64> {
233 self.last
234 }
235
236 pub fn reset(&mut self) {
237 self.prev_close = None;
238 self.avg_gain = None;
239 self.avg_loss = None;
240 self.seed_gains.clear();
241 self.seed_losses.clear();
242 self.last = None;
243 self.bars = 0;
244 }
245}
246
247fn rsi_from_avgs(avg_gain: f64, avg_loss: f64) -> f64 {
248 if avg_loss == 0.0 {
249 return if avg_gain == 0.0 { 50.0 } else { 100.0 };
250 }
251 let rs = avg_gain / avg_loss;
252 100.0 - 100.0 / (1.0 + rs)
253}
254
255pub fn rsi(closes: &[f64], params: RsiParams) -> FinanceResult<RsiSeries> {
256 ValidatedRsi::new(params)?.compute(closes)
257}
258
259fn rsi_validated(closes: &[f64], eng: ValidatedRsi) -> FinanceResult<RsiSeries> {
260 validate_series("close", closes)?;
261 let mut state = RsiState::new(eng.params)?;
262 let rsi = state.push_bars(closes)?;
263 Ok(RsiSeries {
264 rsi,
265 params: eng.params,
266 })
267}
268
269pub fn rsi_solution(closes: &[f64], params: RsiParams) -> FinanceResult<RsiSolution> {
277 let series = rsi(closes, params)?;
278 Ok(RsiSolution {
279 series,
280 closes: closes.to_vec(),
281 formula: format!(
282 "RSI({}) Wilder: 100 - 100/(1 + avg_gain/avg_loss)",
283 params.period
284 ),
285 symbolic_formula: "RSI = 100 - 100/(1+RS); RS = Wilder avg gain / avg loss".to_string(),
286 })
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn rising_path_high_rsi() {
295 let c: Vec<f64> = (0..40).map(|i| 100.0 + i as f64).collect();
296 let s = rsi(&c, RsiParams::period_14()).unwrap();
297 let last = s.last().unwrap();
298 assert!(last > 70.0, "rsi={last}");
299 }
300
301 #[test]
302 fn flat_is_50_after_warmup() {
303 let c = vec![100.0; 30];
304 let s = rsi(&c, RsiParams::period_14()).unwrap();
305 let last = s.last().unwrap();
306 assert!((last - 50.0).abs() < 1e-9);
307 }
308
309 #[test]
310 fn state_parity() {
311 let c: Vec<f64> = (0..50)
312 .map(|i| 100.0 + (i % 5) as f64 * 0.2 - 0.3)
313 .collect();
314 let batch = rsi(&c, RsiParams::period_14()).unwrap();
315 let st = RsiState::from_history(RsiParams::period_14(), &c).unwrap();
316 assert!((batch.last().unwrap() - st.last().unwrap()).abs() < 1e-9);
317 }
318
319 #[test]
320 fn falling_path_low_rsi() {
321 let c: Vec<f64> = (0..40).map(|i| 140.0 - i as f64).collect();
322 let last = rsi(&c, RsiParams::period_14()).unwrap().last().unwrap();
323 assert!(last < 30.0, "rsi={last}");
324 }
325
326 #[test]
327 fn warmup_none_before_period() {
328 let c: Vec<f64> = (0..20).map(|i| 100.0 + i as f64 * 0.1).collect();
329 let s = rsi(&c, RsiParams::period_14()).unwrap();
330 assert!(s.rsi[13].is_none());
331 assert!(s.rsi[14].is_some());
332 }
333
334 #[test]
335 fn empty_series_err() {
336 assert!(rsi(&[], RsiParams::period_14()).is_err());
337 }
338
339 #[test]
340 fn zero_period_err() {
341 assert!(RsiState::new(RsiParams::new(0)).is_err());
342 }
343
344 #[test]
345 fn reset_clears_last() {
346 let c: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
347 let mut st = RsiState::from_history(RsiParams::period_14(), &c).unwrap();
348 assert!(st.last().is_some());
349 st.reset();
350 assert!(st.last().is_none());
351 }
352}