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 Engine (Jaw, Teeth, Lips).
7/// Jaw = 13 SMMA (RMA) on hl2, Teeth = 8 SMMA on hl2, Lips = 5 SMMA on hl2.
8#[derive(Debug, Clone)]
9pub struct AlligatorEngine {
10    jaw_rma: Rma,
11    teeth_rma: Rma,
12    lips_rma: Rma,
13    jaw_window: VecDeque<f64>,
14    teeth_window: VecDeque<f64>,
15    lips_window: VecDeque<f64>,
16}
17
18impl AlligatorEngine {
19    pub fn new() -> Self {
20        Self {
21            jaw_rma: Rma::new(13),
22            teeth_rma: Rma::new(8),
23            lips_rma: Rma::new(5),
24            jaw_window: VecDeque::with_capacity(9),
25            teeth_window: VecDeque::with_capacity(6),
26            lips_window: VecDeque::with_capacity(4),
27        }
28    }
29}
30
31impl Default for AlligatorEngine {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Indicator for AlligatorEngine {
38    fn name(&self) -> &str {
39        "alligator"
40    }
41
42    fn warmup_period(&self) -> usize {
43        13 + 8
44    }
45
46    fn reset(&mut self) {
47        self.jaw_rma.reset();
48        self.teeth_rma.reset();
49        self.lips_rma.reset();
50        self.jaw_window.clear();
51        self.teeth_window.clear();
52        self.lips_window.clear();
53    }
54
55    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
56        let hl2 = (bar.high + bar.low) / 2.0;
57
58        let raw_jaw = self.jaw_rma.update(hl2);
59        let raw_teeth = self.teeth_rma.update(hl2);
60        let raw_lips = self.lips_rma.update(hl2);
61
62        if let (Some(j), Some(t), Some(l)) = (raw_jaw, raw_teeth, raw_lips) {
63            // Apply forward offsets: Jaw 8, Teeth 5, Lips 3. A window holding N+1 values and
64            // reading `front()` yields the value computed N pushes ago, i.e. the desired forward
65            // offset of N bars -- so the window must be allowed to grow to N+1 before trimming.
66            self.jaw_window.push_back(j);
67            if self.jaw_window.len() > 9 {
68                self.jaw_window.pop_front();
69            }
70
71            self.teeth_window.push_back(t);
72            if self.teeth_window.len() > 6 {
73                self.teeth_window.pop_front();
74            }
75
76            self.lips_window.push_back(l);
77            if self.lips_window.len() > 4 {
78                self.lips_window.pop_front();
79            }
80
81            let jaw_val = *self.jaw_window.front().unwrap();
82            let teeth_val = *self.teeth_window.front().unwrap();
83            let lips_val = *self.lips_window.front().unwrap();
84
85            let mut extra = HashMap::new();
86            extra.insert("jaw".to_string(), jaw_val);
87            extra.insert("teeth".to_string(), teeth_val);
88            extra.insert("lips".to_string(), lips_val);
89
90            Some(IndicatorOutput::with_extra(lips_val, extra))
91        } else {
92            None
93        }
94    }
95
96    fn alerts(&self) -> Vec<IndicatorAlert> {
97        Vec::new()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_alligator_basic() {
107        let mut gator = AlligatorEngine::new();
108        let mut out = None;
109        for i in 0..40 {
110            let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0 + i as f64, 1000.0);
111            out = gator.on_bar(&b);
112        }
113        assert!(out.is_some());
114        let o = out.unwrap();
115        assert!(o.extra.contains_key("jaw"));
116        assert!(o.extra.contains_key("teeth"));
117        assert!(o.extra.contains_key("lips"));
118    }
119}