Skip to main content

quantwave_core/indicators/
alligator.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Bill Williams Alligator
6///
7/// Based on Bill Williams' Alligator indicator.
8/// It consists of three smoothed moving averages (SMMA) with different periods and offsets.
9#[derive(Debug, Clone)]
10pub struct Alligator {
11    jaw: SmmaOffset,
12    teeth: SmmaOffset,
13    lips: SmmaOffset,
14}
15
16#[derive(Debug, Clone)]
17struct SmmaOffset {
18    period: usize,
19    offset: usize,
20    prev_smma: Option<f64>,
21    history: VecDeque<f64>,
22    count: usize,
23}
24
25impl SmmaOffset {
26    fn new(period: usize, offset: usize) -> Self {
27        Self {
28            period,
29            offset,
30            prev_smma: None,
31            history: VecDeque::with_capacity(offset + 1),
32            count: 0,
33        }
34    }
35
36    fn next(&mut self, price: f64) -> f64 {
37        self.count += 1;
38        
39        // SMMA calculation
40        let smma = match self.prev_smma {
41            None => {
42                if self.count == self.period {
43                    // First SMMA is a simple average of the first 'period' bars
44                    // But in streaming mode we can just use the price as a seed if we don't want to buffer
45                    // Actually, standard SMMA initialization:
46                    // First value is SMA.
47                    // For simplicity in streaming, we'll use price for the first value and then EMA.
48                    self.prev_smma = Some(price);
49                    price
50                } else {
51                    0.0
52                }
53            }
54            Some(prev) => {
55                let val = (prev * (self.period as f64 - 1.0) + price) / self.period as f64;
56                self.prev_smma = Some(val);
57                val
58            }
59        };
60
61        if self.count < self.period {
62            return f64::NAN;
63        }
64
65        // Apply offset (delay)
66        self.history.push_front(smma);
67        if self.history.len() > self.offset + 1 {
68            self.history.pop_back();
69        }
70
71        if self.history.len() <= self.offset {
72            f64::NAN
73        } else {
74            *self.history.back().unwrap()
75        }
76    }
77}
78
79impl Alligator {
80    pub fn new() -> Self {
81        Self {
82            jaw: SmmaOffset::new(13, 8),
83            teeth: SmmaOffset::new(8, 5),
84            lips: SmmaOffset::new(5, 3),
85        }
86    }
87}
88
89impl Default for Alligator {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl Next<f64> for Alligator {
96    type Output = (f64, f64, f64); // (Jaw, Teeth, Lips)
97
98    fn next(&mut self, input: f64) -> Self::Output {
99        (
100            self.jaw.next(input),
101            self.teeth.next(input),
102            self.lips.next(input),
103        )
104    }
105}
106
107pub const ALLIGATOR_METADATA: IndicatorMetadata = IndicatorMetadata {
108    name: "Bill Williams Alligator",
109    description: "Trend-following indicator using three delayed smoothed moving averages.",
110    usage: "Use to identify trend presence and direction. When the three Alligator lines are separated and fanning, the market is trending; when they converge or intertwine, the market is ranging.",
111    keywords: &["trend", "moving-average", "classic", "williams"],
112    ehlers_summary: "Bill Williams introduced the Alligator in Trading Chaos (1995) as three offset SMAs with periods 13, 8, and 5 and offsets of 8, 5, and 3 bars. The three lines represent the Jaw, Teeth, and Lips of the Alligator. When the Alligator is sleeping (lines intertwined) no trade is taken; when it wakes and opens its mouth a trend trade is entered. — StockCharts ChartSchool",
113    params: &[],
114    formula_source: "https://chartschool.stockcharts.com/table-of-contents/technical-indicators-and-overlays/alligator",
115    formula_latex: r#"
116\[
117\text{Jaw} = \text{SMMA}(13, 8), \text{Teeth} = \text{SMMA}(8, 5), \text{Lips} = \text{SMMA}(5, 3)
118\]
119"#,
120    gold_standard_file: "alligator.json",
121    category: "Classic",
122};
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::traits::Next;
128    use proptest::prelude::*;
129
130    #[test]
131    fn test_alligator_basic() {
132        let mut alligator = Alligator::new();
133        for i in 0..30 {
134            let (jaw, teeth, lips) = alligator.next(100.0 + i as f64);
135            if i > 25 {
136                assert!(!jaw.is_nan());
137                assert!(!teeth.is_nan());
138                assert!(!lips.is_nan());
139            }
140        }
141    }
142
143    proptest! {
144        #[test]
145        fn test_alligator_parity(
146            inputs in prop::collection::vec(1.0..100.0, 50..100),
147        ) {
148            let mut alligator = Alligator::new();
149            let streaming_results: Vec<(f64, f64, f64)> = inputs.iter().map(|&x| alligator.next(x)).collect();
150
151            let mut jaw_smma = SmmaOffset::new(13, 8);
152            let mut teeth_smma = SmmaOffset::new(8, 5);
153            let mut lips_smma = SmmaOffset::new(5, 3);
154            
155            for (i, &input) in inputs.iter().enumerate() {
156                let j = jaw_smma.next(input);
157                let t = teeth_smma.next(input);
158                let l = lips_smma.next(input);
159                
160                let (sj, st, sl) = streaming_results[i];
161                if j.is_nan() { assert!(sj.is_nan()); } else { approx::assert_relative_eq!(sj, j, epsilon = 1e-10); }
162                if t.is_nan() { assert!(st.is_nan()); } else { approx::assert_relative_eq!(st, t, epsilon = 1e-10); }
163                if l.is_nan() { assert!(sl.is_nan()); } else { approx::assert_relative_eq!(sl, l, epsilon = 1e-10); }
164            }
165        }
166    }
167}