Skip to main content

kestrel_chartkit/indicator/
alligator.rs

1use super::smoothing::Rma;
2use super::{Indicator, IndicatorAlert, IndicatorOutput};
3use crate::model::Bar;
4use std::collections::{HashMap, VecDeque};
5
6/// Williams Alligator: three smoothed averages of the median price, shifted forward.
7///
8/// Jaw, teeth and lips are Wilder averages ([`Rma`], seeded with the SMA of their first values)
9/// of `(high + low) / 2` over 13, 8 and 5 bars, shifted forward by 8, 5 and 3 bars: the value
10/// published for a line is the one it had that many outputs earlier. The shift is applied through
11/// a window that only starts filling once all three averages exist, so right after the first
12/// output it is still zero and reaches its full length after 8, 5 and 3 further outputs.
13///
14/// `value` and `extra["lips"]`: the lips; `extra["jaw"]` and `extra["teeth"]`. First output: with
15/// the 13th bar, when the jaw's average exists. [`Indicator::reset`] clears the averages and the
16/// shift windows.
17#[derive(Debug, Clone)]
18pub struct AlligatorEngine {
19    jaw_rma: Rma,
20    teeth_rma: Rma,
21    lips_rma: Rma,
22    jaw_window: VecDeque<f64>,
23    teeth_window: VecDeque<f64>,
24    lips_window: VecDeque<f64>,
25}
26
27impl AlligatorEngine {
28    pub fn new() -> Self {
29        Self {
30            jaw_rma: Rma::new(13),
31            teeth_rma: Rma::new(8),
32            lips_rma: Rma::new(5),
33            jaw_window: VecDeque::with_capacity(9),
34            teeth_window: VecDeque::with_capacity(6),
35            lips_window: VecDeque::with_capacity(4),
36        }
37    }
38}
39
40impl Default for AlligatorEngine {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl Indicator for AlligatorEngine {
47    fn name(&self) -> &str {
48        "alligator"
49    }
50
51    fn warmup_period(&self) -> usize {
52        13 + 8
53    }
54
55    fn reset(&mut self) {
56        self.jaw_rma.reset();
57        self.teeth_rma.reset();
58        self.lips_rma.reset();
59        self.jaw_window.clear();
60        self.teeth_window.clear();
61        self.lips_window.clear();
62    }
63
64    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
65        let hl2 = (bar.high + bar.low) / 2.0;
66
67        let raw_jaw = self.jaw_rma.update(hl2);
68        let raw_teeth = self.teeth_rma.update(hl2);
69        let raw_lips = self.lips_rma.update(hl2);
70
71        if let (Some(j), Some(t), Some(l)) = (raw_jaw, raw_teeth, raw_lips) {
72            // Apply forward offsets: Jaw 8, Teeth 5, Lips 3. A window holding N+1 values and
73            // reading `front()` yields the value computed N pushes ago, i.e. the desired forward
74            // offset of N bars -- so the window must be allowed to grow to N+1 before trimming.
75            self.jaw_window.push_back(j);
76            if self.jaw_window.len() > 9 {
77                self.jaw_window.pop_front();
78            }
79
80            self.teeth_window.push_back(t);
81            if self.teeth_window.len() > 6 {
82                self.teeth_window.pop_front();
83            }
84
85            self.lips_window.push_back(l);
86            if self.lips_window.len() > 4 {
87                self.lips_window.pop_front();
88            }
89
90            let jaw_val = *self.jaw_window.front().unwrap();
91            let teeth_val = *self.teeth_window.front().unwrap();
92            let lips_val = *self.lips_window.front().unwrap();
93
94            let mut extra = HashMap::new();
95            extra.insert("jaw".to_string(), jaw_val);
96            extra.insert("teeth".to_string(), teeth_val);
97            extra.insert("lips".to_string(), lips_val);
98
99            Some(IndicatorOutput::with_extra(lips_val, extra))
100        } else {
101            None
102        }
103    }
104
105    fn alerts(&self) -> Vec<IndicatorAlert> {
106        Vec::new()
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_alligator_basic() {
116        let mut gator = AlligatorEngine::new();
117        let mut out = None;
118        for i in 0..40 {
119            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
120            out = gator.on_bar(&b);
121        }
122        assert!(out.is_some());
123        let o = out.unwrap();
124        assert!(o.extra.contains_key("jaw"));
125        assert!(o.extra.contains_key("teeth"));
126        assert!(o.extra.contains_key("lips"));
127    }
128}