1use crate::aes::Aesthetic;
13use crate::data::{DataFrame, Value};
14use crate::scale::ScaleSet;
15
16use super::Stat;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum MacdPart {
21 Line,
22 Signal,
23 Histogram,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum Indicator {
29 Sma(usize),
31 Ema(usize),
33 Wma(usize),
35 Bollinger(usize, f64),
37 Macd(usize, usize, usize, MacdPart),
39 Rsi(usize),
41 Atr(usize),
43 Vwap,
45 Stochastic(usize, usize),
48 ZigZag(f64),
50}
51
52#[derive(Clone, Debug)]
54pub struct StatIndicator {
55 pub indicator: Indicator,
56}
57
58impl StatIndicator {
59 pub fn new(indicator: Indicator) -> Self {
60 StatIndicator { indicator }
61 }
62}
63
64pub fn sma(v: &[f64], n: usize) -> Vec<Option<f64>> {
66 let n = n.max(1);
67 let mut out = vec![None; v.len()];
68 let mut sum = 0.0;
69 for i in 0..v.len() {
70 sum += v[i];
71 if i >= n {
72 sum -= v[i - n];
73 }
74 if i + 1 >= n {
75 out[i] = Some(sum / n as f64);
76 }
77 }
78 out
79}
80
81pub fn ema(v: &[f64], n: usize) -> Vec<Option<f64>> {
83 smoothed(v, n, 2.0 / (n.max(1) as f64 + 1.0))
84}
85
86fn smoothed(v: &[f64], n: usize, alpha: f64) -> Vec<Option<f64>> {
88 let n = n.max(1);
89 let mut out = vec![None; v.len()];
90 if v.len() < n {
91 return out;
92 }
93 let mut prev = v[..n].iter().sum::<f64>() / n as f64;
94 out[n - 1] = Some(prev);
95 for i in n..v.len() {
96 prev += alpha * (v[i] - prev);
97 out[i] = Some(prev);
98 }
99 out
100}
101
102pub fn wma(v: &[f64], n: usize) -> Vec<Option<f64>> {
104 let n = n.max(1);
105 let denom = (n * (n + 1)) as f64 / 2.0;
106 (0..v.len())
107 .map(|i| {
108 (i + 1 >= n).then(|| {
109 (0..n)
110 .map(|k| v[i + 1 - n + k] * (k + 1) as f64)
111 .sum::<f64>()
112 / denom
113 })
114 })
115 .collect()
116}
117
118pub fn rsi(v: &[f64], n: usize) -> Vec<Option<f64>> {
121 let n = n.max(1);
122 let mut out = vec![None; v.len()];
123 if v.len() <= n {
124 return out;
125 }
126 let ch: Vec<f64> = v.windows(2).map(|w| w[1] - w[0]).collect();
127 let (mut gain, mut loss) = (0.0, 0.0);
128 for &c in &ch[..n] {
129 gain += c.max(0.0);
130 loss += (-c).max(0.0);
131 }
132 gain /= n as f64;
133 loss /= n as f64;
134 let value = |g: f64, l: f64| {
135 if l == 0.0 {
136 if g == 0.0 {
137 50.0
138 } else {
139 100.0
140 }
141 } else {
142 100.0 - 100.0 / (1.0 + g / l)
143 }
144 };
145 out[n] = Some(value(gain, loss));
146 for i in n..ch.len() {
147 gain = (gain * (n as f64 - 1.0) + ch[i].max(0.0)) / n as f64;
148 loss = (loss * (n as f64 - 1.0) + (-ch[i]).max(0.0)) / n as f64;
149 out[i + 1] = Some(value(gain, loss));
150 }
151 out
152}
153
154impl Stat for StatIndicator {
155 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
156 let Some(x) = data.column("x") else {
157 return DataFrame::new();
158 };
159 let num = |name: &str| data.column(name);
160 let price_col = num("close").or_else(|| num("y"));
161 let Some(price_col) = price_col else {
162 return DataFrame::new();
163 };
164 let key = |v: &Value| v.as_f64().unwrap_or(f64::NAN);
165 let mut order: Vec<usize> = (0..data.nrows())
166 .filter(|&i| price_col[i].as_f64().is_some_and(f64::is_finite))
167 .collect();
168 order.sort_by(|&a, &b| key(&x[a]).total_cmp(&key(&x[b])));
169 let take = |c: Option<&[Value]>| -> Option<Vec<f64>> {
170 let c = c?;
171 Some(
172 order
173 .iter()
174 .map(|&i| c[i].as_f64().unwrap_or(f64::NAN))
175 .collect(),
176 )
177 };
178 let p = take(Some(price_col)).unwrap_or_default();
179 let (high, low) = (take(num("high")), take(num("low")));
180 let n_rows = p.len();
181 let mut val: Vec<(Option<f64>, Option<f64>, Option<f64>)> =
183 vec![(None, None, None); n_rows];
184 let set_y = |val: &mut Vec<(Option<f64>, Option<f64>, Option<f64>)>,
185 ys: Vec<Option<f64>>| {
186 for (v, y) in val.iter_mut().zip(ys) {
187 v.0 = y;
188 }
189 };
190 match self.indicator {
191 Indicator::Sma(n) => set_y(&mut val, sma(&p, n)),
192 Indicator::Ema(n) => set_y(&mut val, ema(&p, n)),
193 Indicator::Wma(n) => set_y(&mut val, wma(&p, n)),
194 Indicator::Bollinger(n, k) => {
195 let mid = sma(&p, n);
196 for i in 0..n_rows {
197 if let Some(m) = mid[i] {
198 let w = &p[i + 1 - n.max(1)..=i];
199 let sd = (w.iter().map(|v| (v - m).powi(2)).sum::<f64>() / w.len() as f64)
200 .sqrt();
201 val[i] = (Some(m), Some(m - k * sd), Some(m + k * sd));
202 }
203 }
204 }
205 Indicator::Macd(fast, slow, signal, part) => {
206 let (f, s) = (ema(&p, fast), ema(&p, slow));
207 let line: Vec<Option<f64>> =
208 f.iter().zip(&s).map(|(a, b)| Some((*a)? - (*b)?)).collect();
209 let start = line.iter().position(Option::is_some).unwrap_or(n_rows);
211 let defined: Vec<f64> = line[start..].iter().map(|v| v.unwrap_or(0.0)).collect();
212 let mut sig = vec![None; start];
213 sig.extend(ema(&defined, signal));
214 let out: Vec<Option<f64>> = match part {
215 MacdPart::Line => line.iter().zip(&sig).map(|(l, s)| s.and(*l)).collect(),
216 MacdPart::Signal => sig,
217 MacdPart::Histogram => line
218 .iter()
219 .zip(&sig)
220 .map(|(l, s)| Some((*l)? - (*s)?))
221 .collect(),
222 };
223 set_y(&mut val, out);
224 }
225 Indicator::Rsi(n) => set_y(&mut val, rsi(&p, n)),
226 Indicator::Atr(n) => {
227 if let (Some(h), Some(l)) = (&high, &low) {
228 let tr: Vec<f64> = (0..n_rows)
229 .map(|i| {
230 let hl = h[i] - l[i];
231 if i == 0 {
232 hl
233 } else {
234 hl.max((h[i] - p[i - 1]).abs()).max((l[i] - p[i - 1]).abs())
235 }
236 })
237 .collect();
238 set_y(&mut val, smoothed(&tr, n, 1.0 / n.max(1) as f64));
239 }
240 }
241 Indicator::Vwap => {
242 let vol = take(num("weight")).unwrap_or_else(|| vec![1.0; n_rows]);
243 let (mut pv, mut vv) = (0.0, 0.0);
244 let out = (0..n_rows)
245 .map(|i| {
246 let typical = match (&high, &low) {
247 (Some(h), Some(l)) => (h[i] + l[i] + p[i]) / 3.0,
248 _ => p[i],
249 };
250 pv += typical * vol[i];
251 vv += vol[i];
252 (vv > 0.0).then(|| pv / vv)
253 })
254 .collect();
255 set_y(&mut val, out);
256 }
257 Indicator::Stochastic(k, d) => {
258 if let (Some(h), Some(l)) = (&high, &low) {
259 let k = k.max(1);
260 let raw: Vec<Option<f64>> = (0..n_rows)
261 .map(|i| {
262 (i + 1 >= k).then(|| {
263 let hh = h[i + 1 - k..=i].iter().copied().fold(f64::MIN, f64::max);
264 let ll = l[i + 1 - k..=i].iter().copied().fold(f64::MAX, f64::min);
265 if hh > ll {
266 100.0 * (p[i] - ll) / (hh - ll)
267 } else {
268 50.0
269 }
270 })
271 })
272 .collect();
273 let start = raw.iter().position(Option::is_some).unwrap_or(n_rows);
274 let defined: Vec<f64> = raw[start..].iter().map(|v| v.unwrap_or(0.0)).collect();
275 let mut out = vec![None; start];
276 out.extend(sma(&defined, d));
277 set_y(&mut val, out);
278 }
279 }
280 Indicator::ZigZag(pct) => {
281 let th = pct.abs() / 100.0;
282 if n_rows > 0 {
283 let mut pivots = vec![0usize];
286 let mut ext = 0usize;
287 let mut dir = 0i8;
288 for i in 1..n_rows {
289 match dir {
290 0 => {
291 if p[i] >= p[0] * (1.0 + th) {
292 dir = 1;
293 ext = i;
294 } else if p[i] <= p[0] * (1.0 - th) {
295 dir = -1;
296 ext = i;
297 }
298 }
299 1 => {
300 if p[i] > p[ext] {
301 ext = i;
302 } else if p[i] <= p[ext] * (1.0 - th) {
303 pivots.push(ext);
304 dir = -1;
305 ext = i;
306 }
307 }
308 _ => {
309 if p[i] < p[ext] {
310 ext = i;
311 } else if p[i] >= p[ext] * (1.0 + th) {
312 pivots.push(ext);
313 dir = 1;
314 ext = i;
315 }
316 }
317 }
318 }
319 if *pivots.last().unwrap() != ext {
320 pivots.push(ext);
321 }
322 for i in pivots {
323 val[i].0 = Some(p[i]);
324 }
325 }
326 }
327 }
328 let keep: Vec<usize> = (0..n_rows)
329 .filter(|&i| val[i].0.is_some_and(f64::is_finite))
330 .collect();
331 let mut out = DataFrame::new();
332 out.add_column(
333 "x".into(),
334 keep.iter().map(|&i| x[order[i]].clone()).collect(),
335 );
336 out.add_column(
337 "y".into(),
338 keep.iter()
339 .map(|&i| Value::Float(val[i].0.unwrap()))
340 .collect(),
341 );
342 if matches!(self.indicator, Indicator::Bollinger(..)) {
343 out.add_column(
344 "ymin".into(),
345 keep.iter()
346 .map(|&i| Value::Float(val[i].1.unwrap_or(f64::NAN)))
347 .collect(),
348 );
349 out.add_column(
350 "ymax".into(),
351 keep.iter()
352 .map(|&i| Value::Float(val[i].2.unwrap_or(f64::NAN)))
353 .collect(),
354 );
355 }
356 for c in ["color", "fill", "group", "linetype"] {
357 if let Some(col) = data.column(c) {
358 out.add_column(
359 c.into(),
360 keep.iter().map(|&i| col[order[i]].clone()).collect(),
361 );
362 }
363 }
364 out
365 }
366
367 fn required_aes(&self) -> Vec<Aesthetic> {
368 vec![Aesthetic::X]
369 }
370
371 fn panelwise(&self) -> bool {
373 true
374 }
375
376 fn name(&self) -> &str {
377 "indicator"
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 fn close(a: Option<f64>, b: f64) -> bool {
386 a.is_some_and(|a| (a - b).abs() < 1e-9)
387 }
388
389 #[test]
390 fn moving_averages() {
391 let v = [1.0, 2.0, 3.0, 4.0, 5.0];
392 assert_eq!(
393 sma(&v, 3),
394 vec![None, None, Some(2.0), Some(3.0), Some(4.0)]
395 );
396 let e = ema(&v, 3);
397 assert!(close(e[2], 2.0) && close(e[3], 3.0) && close(e[4], 4.0));
399 let w = wma(&v, 3);
400 assert!(close(w[2], (1.0 + 4.0 + 9.0) / 6.0));
401 }
402
403 #[test]
404 fn rsi_extremes_and_wilder() {
405 let up: Vec<f64> = (0..20).map(f64::from).collect();
406 assert!(rsi(&up, 14).iter().flatten().all(|&r| r == 100.0));
407 let flat = vec![5.0; 20];
408 assert!(rsi(&flat, 14).iter().flatten().all(|&r| r == 50.0));
409 let zig: Vec<f64> = (0..30)
411 .map(|i| if i % 2 == 0 { 10.0 } else { 11.0 })
412 .collect();
413 let r = rsi(&zig, 14);
414 assert!(r[14].is_some_and(|v| (v - 50.0).abs() < 4.0));
415 }
416}