summaryrefslogtreecommitdiff
path: root/src/indicators.rs
blob: 43787573bc33ce2d4eb703d30d6181b72c97910a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
use crate::data::Candle;
use ta::indicators::FastStochastic;
use ta::{DataItem, Next};

/// Wilder RSI from closes: first average = simple mean of the first `period`
/// changes, then avg = (prev * (period-1) + current) / period. (`ta`'s RSI uses a
/// standard EMA instead, which differs from charting software.) The first
/// `period` values are `None`.
pub fn rsi(candles: &[Candle], period: usize) -> Vec<Option<f64>> {
    let n = period as f64;
    let mut out = vec![None; candles.len()];
    let (mut gain, mut loss) = (0.0, 0.0);
    for i in 1..candles.len() {
        let ch = candles[i].close - candles[i - 1].close;
        let (g, l) = (ch.max(0.0), (-ch).max(0.0));
        if i <= period {
            gain += g / n;
            loss += l / n;
        } else {
            gain = (gain * (n - 1.0) + g) / n;
            loss = (loss * (n - 1.0) + l) / n;
        }
        if i >= period {
            out[i] = Some(if loss == 0.0 { if gain == 0.0 { 50.0 } else { 100.0 } } else { 100.0 - 100.0 / (1.0 + gain / loss) });
        }
    }
    out
}

/// KD: RSV from `ta`'s FastStochastic over high/low/close, then
/// K = 2/3 K_prev + 1/3 RSV, D = 2/3 D_prev + 1/3 K, both seeded at 50.
pub fn kd(candles: &[Candle], period: usize) -> (Vec<Option<f64>>, Vec<Option<f64>>) {
    let mut rsv_ind = FastStochastic::new(period).expect("period > 0");
    let (mut k, mut d) = (50.0, 50.0);
    let mut ks = Vec::with_capacity(candles.len());
    let mut ds = Vec::with_capacity(candles.len());
    for (i, c) in candles.iter().enumerate() {
        let item = DataItem::builder()
            .open(c.open)
            .high(c.high)
            .low(c.low)
            .close(c.close)
            .volume(c.volume)
            .build()
            .expect("validated candle");
        let rsv = rsv_ind.next(&item);
        if i + 1 < period {
            ks.push(None);
            ds.push(None);
            continue;
        }
        k = k * 2.0 / 3.0 + rsv / 3.0;
        d = d * 2.0 / 3.0 + k / 3.0;
        ks.push(Some(k));
        ds.push(Some(d));
    }
    (ks, ds)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn candle(h: f64, l: f64, c: f64) -> Candle {
        Candle { date: "2024-01-01".into(), open: c, high: h, low: l, close: c, volume: 1.0 }
    }

    #[test]
    fn kd_hand_computed() {
        // period 2. i=1: window highs {12,14}->14, lows {8,9}->8, close 13 -> RSV = 5/6*100.
        let cs = [candle(12.0, 8.0, 10.0), candle(14.0, 9.0, 13.0)];
        let (k, d) = kd(&cs, 2);
        assert_eq!(k[0], None);
        let rsv = 5.0 / 6.0 * 100.0;
        let k1 = 50.0 * 2.0 / 3.0 + rsv / 3.0;
        assert!((k[1].unwrap() - k1).abs() < 1e-9);
        assert!((d[1].unwrap() - (50.0 * 2.0 / 3.0 + k1 / 3.0)).abs() < 1e-9);
    }

    #[test]
    fn rsi_wilder_hand_computed() {
        // period 2, closes 10, 11, 10, 12: first avg gain=.5 loss=.5 (i=2) -> RSI 50;
        // i=3: gain=(.5*1+2)/2=1.25, loss=(.5*1+0)/2=.25 -> RS 5 -> RSI 100-100/6
        let cs = [10.0, 11.0, 10.0, 12.0].map(|c| candle(c, c, c));
        let r = rsi(&cs, 2);
        assert!((r[2].unwrap() - 50.0).abs() < 1e-9);
        assert!((r[3].unwrap() - (100.0 - 100.0 / 6.0)).abs() < 1e-9);
    }

    #[test]
    fn rsi_all_gains_is_100() {
        let cs: Vec<_> = (1..=20).map(|i| candle(i as f64 + 1.0, i as f64, i as f64)).collect();
        let r = rsi(&cs, 14);
        assert_eq!(r[13], None);
        assert!(r[14].is_some());
        assert!((r[19].unwrap() - 100.0).abs() < 1e-9);
    }
}