Skip to main content

wickra_core/indicators/
alligator.rs

1//! Bill Williams' Alligator indicator.
2
3use crate::error::{Error, Result};
4use crate::indicators::smma::Smma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8/// Alligator output: three smoothed moving averages of the median price
9/// `(high + low) / 2`.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AlligatorOutput {
12    /// `Jaw` — the slowest line (default period 13).
13    pub jaw: f64,
14    /// `Teeth` — the middle line (default period 8).
15    pub teeth: f64,
16    /// `Lips` — the fastest line (default period 5).
17    pub lips: f64,
18}
19
20/// Bill Williams' Alligator: three `SMMA`s of the median price `(high + low) / 2`
21/// with different periods. Classic parameters are `(jaw = 13, teeth = 8, lips = 5)`.
22///
23/// The original chart variant additionally shifts each line forward by a fixed
24/// number of bars for display (Jaw +8, Teeth +5, Lips +3). Wickra publishes the
25/// *unshifted* `SMMA` values — the consumer can apply the visual shift on the
26/// chart side. The indicator emits values once all three `SMMA`s have warmed
27/// up, i.e. after `max(jaw, teeth, lips) = jaw` candles.
28///
29/// Reference: Bill Williams, *Trading Chaos*, 1995.
30///
31/// # Example
32///
33/// ```
34/// use wickra_core::{Alligator, Candle, Indicator};
35///
36/// let mut alligator = Alligator::classic();
37/// let mut last = None;
38/// for i in 0..40 {
39///     let base = 100.0 + f64::from(i);
40///     let candle =
41///         Candle::new(base, base + 1.0, base - 1.0, base, 1.0, i64::from(i)).unwrap();
42///     last = alligator.update(candle);
43/// }
44/// assert!(last.is_some());
45/// ```
46#[derive(Debug, Clone)]
47pub struct Alligator {
48    jaw_period: usize,
49    teeth_period: usize,
50    lips_period: usize,
51    jaw: Smma,
52    teeth: Smma,
53    lips: Smma,
54}
55
56impl Alligator {
57    /// # Errors
58    /// Returns [`Error::PeriodZero`] if any period is zero.
59    pub fn new(jaw_period: usize, teeth_period: usize, lips_period: usize) -> Result<Self> {
60        if jaw_period == 0 || teeth_period == 0 || lips_period == 0 {
61            return Err(Error::PeriodZero);
62        }
63        Ok(Self {
64            jaw_period,
65            teeth_period,
66            lips_period,
67            jaw: Smma::new(jaw_period)?,
68            teeth: Smma::new(teeth_period)?,
69            lips: Smma::new(lips_period)?,
70        })
71    }
72
73    /// Bill Williams' classic parameters: `(jaw = 13, teeth = 8, lips = 5)`.
74    pub fn classic() -> Self {
75        Self::new(13, 8, 5).expect("classic Alligator parameters are valid")
76    }
77
78    /// Configured `(jaw_period, teeth_period, lips_period)`.
79    pub const fn periods(&self) -> (usize, usize, usize) {
80        (self.jaw_period, self.teeth_period, self.lips_period)
81    }
82}
83
84impl Indicator for Alligator {
85    type Input = Candle;
86    type Output = AlligatorOutput;
87
88    #[inline]
89    fn update(&mut self, candle: Candle) -> Option<AlligatorOutput> {
90        let median = f64::midpoint(candle.high, candle.low);
91        // Feed every `SMMA` on every bar so they warm up in parallel; gating
92        // the longer lines behind the shorter ones would starve them during
93        // their own warmup.
94        let lips = self.lips.update(median);
95        let teeth = self.teeth.update(median);
96        let jaw = self.jaw.update(median);
97        Some(AlligatorOutput {
98            jaw: jaw?,
99            teeth: teeth?,
100            lips: lips?,
101        })
102    }
103
104    fn reset(&mut self) {
105        self.jaw.reset();
106        self.teeth.reset();
107        self.lips.reset();
108    }
109
110    #[inline]
111    fn warmup_period(&self) -> usize {
112        // All three SMMAs run on every bar, so readiness is gated by the
113        // longest period — the Jaw with the default parameters.
114        self.jaw_period.max(self.teeth_period).max(self.lips_period)
115    }
116
117    #[inline]
118    fn is_ready(&self) -> bool {
119        self.jaw.is_ready() && self.teeth.is_ready() && self.lips.is_ready()
120    }
121
122    #[inline]
123    fn name(&self) -> &'static str {
124        "Alligator"
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::traits::BatchExt;
132    use approx::assert_relative_eq;
133
134    fn candle(high: f64, low: f64, ts: i64) -> Candle {
135        let close = f64::midpoint(high, low);
136        Candle::new(close, high, low, close, 1.0, ts).unwrap()
137    }
138
139    #[test]
140    fn rejects_zero_period() {
141        assert!(matches!(Alligator::new(0, 8, 5), Err(Error::PeriodZero)));
142        assert!(matches!(Alligator::new(13, 0, 5), Err(Error::PeriodZero)));
143        assert!(matches!(Alligator::new(13, 8, 0), Err(Error::PeriodZero)));
144    }
145
146    #[test]
147    fn accessors_and_metadata() {
148        let alligator = Alligator::classic();
149        assert_eq!(alligator.periods(), (13, 8, 5));
150        assert_eq!(alligator.warmup_period(), 13);
151        assert_eq!(alligator.name(), "Alligator");
152    }
153
154    #[test]
155    fn constant_series_yields_the_constant() {
156        // Median price = 10 for every bar, so each SMMA seeds to 10 and stays.
157        let mut alligator = Alligator::classic();
158        let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
159        let out = alligator.batch(&candles);
160        for v in out.iter().skip(12).flatten() {
161            assert_relative_eq!(v.jaw, 10.0, epsilon = 1e-12);
162            assert_relative_eq!(v.teeth, 10.0, epsilon = 1e-12);
163            assert_relative_eq!(v.lips, 10.0, epsilon = 1e-12);
164        }
165    }
166
167    #[test]
168    fn warmup_emits_first_value_at_longest_period() {
169        let mut alligator = Alligator::new(5, 3, 2).unwrap();
170        let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, i)).collect();
171        let out = alligator.batch(&candles);
172        for v in out.iter().take(4) {
173            assert!(v.is_none());
174        }
175        assert!(out[4].is_some());
176    }
177
178    #[test]
179    fn pure_uptrend_ordering() {
180        // On a clean uptrend the fastest line (Lips, smallest SMMA) leads the
181        // slowest line (Jaw) — lips > teeth > jaw at the latest bar.
182        let mut alligator = Alligator::classic();
183        let candles: Vec<Candle> = (0_i64..80)
184            .map(|i| candle(10.0 + i as f64, 9.0 + i as f64, i))
185            .collect();
186        let out = alligator.batch(&candles);
187        let last = out.last().unwrap().unwrap();
188        assert!(
189            last.lips > last.teeth,
190            "lips {} > teeth {}",
191            last.lips,
192            last.teeth
193        );
194        assert!(
195            last.teeth > last.jaw,
196            "teeth {} > jaw {}",
197            last.teeth,
198            last.jaw
199        );
200    }
201
202    #[test]
203    fn batch_equals_streaming() {
204        let candles: Vec<Candle> = (0..80_i64)
205            .map(|i| {
206                let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
207                candle(base + 1.0, base - 1.0, i)
208            })
209            .collect();
210        let mut a = Alligator::classic();
211        let mut b = Alligator::classic();
212        assert_eq!(
213            a.batch(&candles),
214            candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
215        );
216    }
217
218    #[test]
219    fn reset_clears_state() {
220        let mut alligator = Alligator::classic();
221        let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
222        alligator.batch(&candles);
223        assert!(alligator.is_ready());
224        alligator.reset();
225        assert!(!alligator.is_ready());
226    }
227}