Skip to main content

termichart_data/
indicators.rs

1use termichart_core::{Candle, Indicator, Point};
2
3// ---------------------------------------------------------------------------
4// SMA
5// ---------------------------------------------------------------------------
6
7/// Simple Moving Average indicator.
8pub struct SmaIndicator {
9    period: usize,
10    name: String,
11}
12
13impl SmaIndicator {
14    pub fn new(period: usize) -> Self {
15        Self {
16            period,
17            name: format!("SMA({})", period),
18        }
19    }
20}
21
22impl Indicator for SmaIndicator {
23    fn name(&self) -> &str {
24        &self.name
25    }
26
27    fn compute(&self, candles: &[Candle]) -> Vec<Point> {
28        if candles.len() < self.period || self.period == 0 {
29            return Vec::new();
30        }
31
32        let mut points = Vec::with_capacity(candles.len() - self.period + 1);
33
34        // Compute the initial window sum.
35        let mut sum: f64 = candles[..self.period].iter().map(|c| c.close).sum();
36        points.push(Point {
37            x: candles[self.period - 1].time,
38            y: sum / self.period as f64,
39        });
40
41        // Slide the window.
42        for i in self.period..candles.len() {
43            sum += candles[i].close - candles[i - self.period].close;
44            points.push(Point {
45                x: candles[i].time,
46                y: sum / self.period as f64,
47            });
48        }
49
50        points
51    }
52}
53
54// ---------------------------------------------------------------------------
55// EMA
56// ---------------------------------------------------------------------------
57
58/// Exponential Moving Average indicator.
59pub struct EmaIndicator {
60    period: usize,
61    name: String,
62}
63
64impl EmaIndicator {
65    pub fn new(period: usize) -> Self {
66        Self {
67            period,
68            name: format!("EMA({})", period),
69        }
70    }
71}
72
73impl Indicator for EmaIndicator {
74    fn name(&self) -> &str {
75        &self.name
76    }
77
78    fn compute(&self, candles: &[Candle]) -> Vec<Point> {
79        if candles.len() < self.period || self.period == 0 {
80            return Vec::new();
81        }
82
83        let multiplier = 2.0 / (self.period as f64 + 1.0);
84        let mut points = Vec::with_capacity(candles.len() - self.period + 1);
85
86        // First EMA value = SMA of the first `period` candles.
87        let sma: f64 =
88            candles[..self.period].iter().map(|c| c.close).sum::<f64>() / self.period as f64;
89        let mut prev_ema = sma;
90
91        points.push(Point {
92            x: candles[self.period - 1].time,
93            y: prev_ema,
94        });
95
96        // Subsequent values use the EMA formula.
97        for candle in &candles[self.period..] {
98            let ema = (candle.close - prev_ema) * multiplier + prev_ema;
99            points.push(Point {
100                x: candle.time,
101                y: ema,
102            });
103            prev_ema = ema;
104        }
105
106        points
107    }
108}
109
110// ---------------------------------------------------------------------------
111// VWAP
112// ---------------------------------------------------------------------------
113
114/// Volume-Weighted Average Price indicator.
115pub struct VwapIndicator;
116
117impl VwapIndicator {
118    pub fn new() -> Self {
119        Self
120    }
121}
122
123impl Default for VwapIndicator {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl Indicator for VwapIndicator {
130    fn name(&self) -> &str {
131        "VWAP"
132    }
133
134    fn compute(&self, candles: &[Candle]) -> Vec<Point> {
135        let mut points = Vec::with_capacity(candles.len());
136        let mut cumulative_tp_vol = 0.0_f64;
137        let mut cumulative_vol = 0.0_f64;
138
139        for candle in candles {
140            let typical_price = (candle.high + candle.low + candle.close) / 3.0;
141            cumulative_tp_vol += typical_price * candle.volume;
142            cumulative_vol += candle.volume;
143
144            let vwap = if cumulative_vol != 0.0 {
145                cumulative_tp_vol / cumulative_vol
146            } else {
147                0.0
148            };
149
150            points.push(Point {
151                x: candle.time,
152                y: vwap,
153            });
154        }
155
156        points
157    }
158}
159
160// ---------------------------------------------------------------------------
161// Bollinger Bands
162// ---------------------------------------------------------------------------
163
164/// Bollinger Bands indicator: middle band (SMA), upper band, lower band.
165pub struct BollingerBandsIndicator {
166    period: usize,
167    std_dev_mult: f64,
168    name: String,
169}
170
171impl BollingerBandsIndicator {
172    pub fn new(period: usize, std_dev_mult: f64) -> Self {
173        Self {
174            period,
175            std_dev_mult,
176            name: format!("BB({},{})", period, std_dev_mult),
177        }
178    }
179
180    /// Returns (middle, upper, lower) bands as three sets of points.
181    pub fn compute_bands(&self, candles: &[Candle]) -> (Vec<Point>, Vec<Point>, Vec<Point>) {
182        if candles.len() < self.period || self.period == 0 {
183            return (Vec::new(), Vec::new(), Vec::new());
184        }
185
186        let mut middle = Vec::with_capacity(candles.len() - self.period + 1);
187        let mut upper = Vec::with_capacity(candles.len() - self.period + 1);
188        let mut lower = Vec::with_capacity(candles.len() - self.period + 1);
189
190        for i in (self.period - 1)..candles.len() {
191            let start = i + 1 - self.period;
192            let slice = &candles[start..=i];
193            let mean: f64 = slice.iter().map(|c| c.close).sum::<f64>() / self.period as f64;
194            let variance: f64 =
195                slice.iter().map(|c| (c.close - mean).powi(2)).sum::<f64>() / self.period as f64;
196            let std_dev = variance.sqrt();
197
198            let time = candles[i].time;
199            middle.push(Point { x: time, y: mean });
200            upper.push(Point {
201                x: time,
202                y: mean + self.std_dev_mult * std_dev,
203            });
204            lower.push(Point {
205                x: time,
206                y: mean - self.std_dev_mult * std_dev,
207            });
208        }
209
210        (middle, upper, lower)
211    }
212}
213
214impl Indicator for BollingerBandsIndicator {
215    fn name(&self) -> &str {
216        &self.name
217    }
218
219    /// Returns the middle band points (SMA). Use compute_bands() for all three bands.
220    fn compute(&self, candles: &[Candle]) -> Vec<Point> {
221        let (middle, _, _) = self.compute_bands(candles);
222        middle
223    }
224}
225
226// ---------------------------------------------------------------------------
227// RSI
228// ---------------------------------------------------------------------------
229
230/// Relative Strength Index indicator.
231pub struct RsiIndicator {
232    period: usize,
233    name: String,
234}
235
236impl RsiIndicator {
237    pub fn new(period: usize) -> Self {
238        Self {
239            period,
240            name: format!("RSI({})", period),
241        }
242    }
243}
244
245impl Indicator for RsiIndicator {
246    fn name(&self) -> &str {
247        &self.name
248    }
249
250    fn compute(&self, candles: &[Candle]) -> Vec<Point> {
251        if candles.len() < self.period + 1 || self.period == 0 {
252            return Vec::new();
253        }
254
255        let mut points = Vec::with_capacity(candles.len() - self.period);
256
257        // Calculate initial average gain and loss
258        let mut avg_gain = 0.0_f64;
259        let mut avg_loss = 0.0_f64;
260
261        for i in 1..=self.period {
262            let change = candles[i].close - candles[i - 1].close;
263            if change > 0.0 {
264                avg_gain += change;
265            } else {
266                avg_loss += change.abs();
267            }
268        }
269
270        avg_gain /= self.period as f64;
271        avg_loss /= self.period as f64;
272
273        let rs = if avg_loss == 0.0 {
274            100.0
275        } else {
276            avg_gain / avg_loss
277        };
278        let rsi = 100.0 - (100.0 / (1.0 + rs));
279        points.push(Point {
280            x: candles[self.period].time,
281            y: rsi,
282        });
283
284        // Subsequent values use smoothed averages
285        for i in (self.period + 1)..candles.len() {
286            let change = candles[i].close - candles[i - 1].close;
287            let (gain, loss) = if change > 0.0 {
288                (change, 0.0)
289            } else {
290                (0.0, change.abs())
291            };
292
293            avg_gain = (avg_gain * (self.period as f64 - 1.0) + gain) / self.period as f64;
294            avg_loss = (avg_loss * (self.period as f64 - 1.0) + loss) / self.period as f64;
295
296            let rs = if avg_loss == 0.0 {
297                100.0
298            } else {
299                avg_gain / avg_loss
300            };
301            let rsi = 100.0 - (100.0 / (1.0 + rs));
302            points.push(Point {
303                x: candles[i].time,
304                y: rsi,
305            });
306        }
307
308        points
309    }
310}
311
312// ---------------------------------------------------------------------------
313// MACD
314// ---------------------------------------------------------------------------
315
316/// MACD (Moving Average Convergence Divergence) indicator.
317pub struct MacdIndicator {
318    fast_period: usize,
319    slow_period: usize,
320    signal_period: usize,
321    name: String,
322}
323
324impl MacdIndicator {
325    pub fn new(fast: usize, slow: usize, signal: usize) -> Self {
326        Self {
327            fast_period: fast,
328            slow_period: slow,
329            signal_period: signal,
330            name: format!("MACD({},{},{})", fast, slow, signal),
331        }
332    }
333
334    /// Returns (macd_line, signal_line, histogram) as three sets of points.
335    pub fn compute_full(&self, candles: &[Candle]) -> (Vec<Point>, Vec<Point>, Vec<Point>) {
336        let fast_ema = EmaIndicator::new(self.fast_period);
337        let slow_ema = EmaIndicator::new(self.slow_period);
338
339        let fast_points = fast_ema.compute(candles);
340        let slow_points = slow_ema.compute(candles);
341
342        if fast_points.is_empty() || slow_points.is_empty() {
343            return (Vec::new(), Vec::new(), Vec::new());
344        }
345
346        // Align by time -- find matching points
347        let mut macd_line = Vec::new();
348        let mut slow_idx = 0;
349        for fp in &fast_points {
350            while slow_idx < slow_points.len() && slow_points[slow_idx].x < fp.x {
351                slow_idx += 1;
352            }
353            if slow_idx < slow_points.len() && (slow_points[slow_idx].x - fp.x).abs() < 0.001 {
354                macd_line.push(Point {
355                    x: fp.x,
356                    y: fp.y - slow_points[slow_idx].y,
357                });
358            }
359        }
360
361        if macd_line.len() < self.signal_period {
362            return (macd_line, Vec::new(), Vec::new());
363        }
364
365        // Compute signal line as EMA of MACD line
366        let multiplier = 2.0 / (self.signal_period as f64 + 1.0);
367        let sma: f64 = macd_line[..self.signal_period]
368            .iter()
369            .map(|p| p.y)
370            .sum::<f64>()
371            / self.signal_period as f64;
372        let mut prev_signal = sma;
373
374        let mut signal_line = Vec::with_capacity(macd_line.len() - self.signal_period + 1);
375        signal_line.push(Point {
376            x: macd_line[self.signal_period - 1].x,
377            y: prev_signal,
378        });
379
380        for p in &macd_line[self.signal_period..] {
381            let signal = (p.y - prev_signal) * multiplier + prev_signal;
382            signal_line.push(Point {
383                x: p.x,
384                y: signal,
385            });
386            prev_signal = signal;
387        }
388
389        // Compute histogram
390        let mut histogram = Vec::new();
391        let mut sig_idx = 0;
392        for mp in &macd_line {
393            while sig_idx < signal_line.len() && signal_line[sig_idx].x < mp.x {
394                sig_idx += 1;
395            }
396            if sig_idx < signal_line.len() && (signal_line[sig_idx].x - mp.x).abs() < 0.001 {
397                histogram.push(Point {
398                    x: mp.x,
399                    y: mp.y - signal_line[sig_idx].y,
400                });
401            }
402        }
403
404        (macd_line, signal_line, histogram)
405    }
406}
407
408impl Indicator for MacdIndicator {
409    fn name(&self) -> &str {
410        &self.name
411    }
412
413    /// Returns the MACD line points. Use compute_full() for all three components.
414    fn compute(&self, candles: &[Candle]) -> Vec<Point> {
415        let (macd_line, _, _) = self.compute_full(candles);
416        macd_line
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    fn sample_candles() -> Vec<Candle> {
425        vec![
426            Candle { time: 1.0, open: 10.0, high: 12.0, low: 9.0, close: 11.0, volume: 100.0 },
427            Candle { time: 2.0, open: 11.0, high: 13.0, low: 10.0, close: 12.0, volume: 150.0 },
428            Candle { time: 3.0, open: 12.0, high: 14.0, low: 11.0, close: 13.0, volume: 200.0 },
429            Candle { time: 4.0, open: 13.0, high: 15.0, low: 12.0, close: 14.0, volume: 120.0 },
430            Candle { time: 5.0, open: 14.0, high: 16.0, low: 13.0, close: 15.0, volume: 180.0 },
431        ]
432    }
433
434    #[test]
435    fn sma_basic() {
436        let candles = sample_candles();
437        let sma = SmaIndicator::new(3);
438        let points = sma.compute(&candles);
439        // SMA(3) starts at index 2: (11+12+13)/3 = 12.0
440        assert_eq!(points.len(), 3);
441        assert!((points[0].y - 12.0).abs() < 1e-10);
442        assert_eq!(points[0].x, 3.0);
443        // Second: (12+13+14)/3 = 13.0
444        assert!((points[1].y - 13.0).abs() < 1e-10);
445        // Third: (13+14+15)/3 = 14.0
446        assert!((points[2].y - 14.0).abs() < 1e-10);
447    }
448
449    #[test]
450    fn sma_insufficient_data() {
451        let candles = sample_candles();
452        let sma = SmaIndicator::new(10);
453        assert!(sma.compute(&candles).is_empty());
454    }
455
456    #[test]
457    fn ema_basic() {
458        let candles = sample_candles();
459        let ema = EmaIndicator::new(3);
460        let points = ema.compute(&candles);
461        assert_eq!(points.len(), 3);
462        // First EMA = SMA(3) = 12.0
463        assert!((points[0].y - 12.0).abs() < 1e-10);
464        // multiplier = 2/(3+1) = 0.5
465        // Second: (14 - 12) * 0.5 + 12 = 13.0
466        assert!((points[1].y - 13.0).abs() < 1e-10);
467        // Third: (15 - 13) * 0.5 + 13 = 14.0
468        assert!((points[2].y - 14.0).abs() < 1e-10);
469    }
470
471    #[test]
472    fn ema_name() {
473        let ema = EmaIndicator::new(20);
474        assert_eq!(ema.name(), "EMA(20)");
475    }
476
477    #[test]
478    fn vwap_basic() {
479        let candles = sample_candles();
480        let vwap = VwapIndicator::new();
481        let points = vwap.compute(&candles);
482        assert_eq!(points.len(), 5);
483
484        // First candle: tp = (12+9+11)/3 = 32/3 ≈ 10.6667
485        // vwap = 10.6667 * 100 / 100 = 10.6667
486        let tp1 = (12.0 + 9.0 + 11.0) / 3.0;
487        assert!((points[0].y - tp1).abs() < 1e-10);
488    }
489
490    #[test]
491    fn vwap_name() {
492        let vwap = VwapIndicator::new();
493        assert_eq!(vwap.name(), "VWAP");
494    }
495
496    #[test]
497    fn bb_basic() {
498        let candles = sample_candles();
499        let bb = BollingerBandsIndicator::new(3, 2.0);
500        let (middle, upper, lower) = bb.compute_bands(&candles);
501        assert_eq!(middle.len(), 3);
502        assert_eq!(upper.len(), 3);
503        assert_eq!(lower.len(), 3);
504        // Middle should be SMA
505        assert!((middle[0].y - 12.0).abs() < 1e-10);
506        // Upper > middle > lower
507        assert!(upper[0].y > middle[0].y);
508        assert!(lower[0].y < middle[0].y);
509    }
510
511    #[test]
512    fn rsi_basic() {
513        let candles = sample_candles();
514        let rsi = RsiIndicator::new(3);
515        let points = rsi.compute(&candles);
516        assert!(!points.is_empty());
517        // RSI should be between 0 and 100
518        for p in &points {
519            assert!(p.y >= 0.0 && p.y <= 100.0);
520        }
521    }
522
523    #[test]
524    fn rsi_all_gains() {
525        // All prices increasing -> RSI should be near 100
526        let candles: Vec<Candle> = (0..10)
527            .map(|i| Candle {
528                time: i as f64,
529                open: i as f64,
530                high: i as f64 + 1.0,
531                low: i as f64,
532                close: i as f64 + 1.0,
533                volume: 100.0,
534            })
535            .collect();
536        let rsi = RsiIndicator::new(3);
537        let points = rsi.compute(&candles);
538        if let Some(last) = points.last() {
539            assert!(last.y > 90.0);
540        }
541    }
542
543    #[test]
544    fn macd_basic() {
545        // Need more data for MACD
546        let candles: Vec<Candle> = (0..30)
547            .map(|i| Candle {
548                time: i as f64,
549                open: 100.0 + (i as f64).sin() * 5.0,
550                high: 105.0 + (i as f64).sin() * 5.0,
551                low: 95.0 + (i as f64).sin() * 5.0,
552                close: 100.0 + (i as f64).sin() * 5.0 + 0.5,
553                volume: 100.0,
554            })
555            .collect();
556        let macd = MacdIndicator::new(12, 26, 9);
557        let (macd_line, _signal, _hist) = macd.compute_full(&candles);
558        // With 30 candles we should get some MACD points
559        assert!(!macd_line.is_empty());
560    }
561
562    #[test]
563    fn macd_name() {
564        let macd = MacdIndicator::new(12, 26, 9);
565        assert_eq!(macd.name(), "MACD(12,26,9)");
566    }
567
568    #[test]
569    fn bb_insufficient_data() {
570        let candles = vec![sample_candles()[0]];
571        let bb = BollingerBandsIndicator::new(5, 2.0);
572        let (m, u, l) = bb.compute_bands(&candles);
573        assert!(m.is_empty());
574        assert!(u.is_empty());
575        assert!(l.is_empty());
576    }
577}