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    fn update(&mut self, candle: Candle) -> Option<AlligatorOutput> {
89        let median = f64::midpoint(candle.high, candle.low);
90        // Feed every `SMMA` on every bar so they warm up in parallel; gating
91        // the longer lines behind the shorter ones would starve them during
92        // their own warmup.
93        let lips = self.lips.update(median);
94        let teeth = self.teeth.update(median);
95        let jaw = self.jaw.update(median);
96        Some(AlligatorOutput {
97            jaw: jaw?,
98            teeth: teeth?,
99            lips: lips?,
100        })
101    }
102
103    fn reset(&mut self) {
104        self.jaw.reset();
105        self.teeth.reset();
106        self.lips.reset();
107    }
108
109    fn warmup_period(&self) -> usize {
110        // All three SMMAs run on every bar, so readiness is gated by the
111        // longest period — the Jaw with the default parameters.
112        self.jaw_period.max(self.teeth_period).max(self.lips_period)
113    }
114
115    fn is_ready(&self) -> bool {
116        self.jaw.is_ready() && self.teeth.is_ready() && self.lips.is_ready()
117    }
118
119    fn name(&self) -> &'static str {
120        "Alligator"
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::traits::BatchExt;
128    use approx::assert_relative_eq;
129
130    fn candle(high: f64, low: f64, ts: i64) -> Candle {
131        let close = f64::midpoint(high, low);
132        Candle::new(close, high, low, close, 1.0, ts).unwrap()
133    }
134
135    #[test]
136    fn rejects_zero_period() {
137        assert!(matches!(Alligator::new(0, 8, 5), Err(Error::PeriodZero)));
138        assert!(matches!(Alligator::new(13, 0, 5), Err(Error::PeriodZero)));
139        assert!(matches!(Alligator::new(13, 8, 0), Err(Error::PeriodZero)));
140    }
141
142    #[test]
143    fn accessors_and_metadata() {
144        let alligator = Alligator::classic();
145        assert_eq!(alligator.periods(), (13, 8, 5));
146        assert_eq!(alligator.warmup_period(), 13);
147        assert_eq!(alligator.name(), "Alligator");
148    }
149
150    #[test]
151    fn constant_series_yields_the_constant() {
152        // Median price = 10 for every bar, so each SMMA seeds to 10 and stays.
153        let mut alligator = Alligator::classic();
154        let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
155        let out = alligator.batch(&candles);
156        for v in out.iter().skip(12).flatten() {
157            assert_relative_eq!(v.jaw, 10.0, epsilon = 1e-12);
158            assert_relative_eq!(v.teeth, 10.0, epsilon = 1e-12);
159            assert_relative_eq!(v.lips, 10.0, epsilon = 1e-12);
160        }
161    }
162
163    #[test]
164    fn warmup_emits_first_value_at_longest_period() {
165        let mut alligator = Alligator::new(5, 3, 2).unwrap();
166        let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 9.0, i)).collect();
167        let out = alligator.batch(&candles);
168        for v in out.iter().take(4) {
169            assert!(v.is_none());
170        }
171        assert!(out[4].is_some());
172    }
173
174    #[test]
175    fn pure_uptrend_ordering() {
176        // On a clean uptrend the fastest line (Lips, smallest SMMA) leads the
177        // slowest line (Jaw) — lips > teeth > jaw at the latest bar.
178        let mut alligator = Alligator::classic();
179        let candles: Vec<Candle> = (0_i64..80)
180            .map(|i| candle(10.0 + i as f64, 9.0 + i as f64, i))
181            .collect();
182        let out = alligator.batch(&candles);
183        let last = out.last().unwrap().unwrap();
184        assert!(
185            last.lips > last.teeth,
186            "lips {} > teeth {}",
187            last.lips,
188            last.teeth
189        );
190        assert!(
191            last.teeth > last.jaw,
192            "teeth {} > jaw {}",
193            last.teeth,
194            last.jaw
195        );
196    }
197
198    #[test]
199    fn batch_equals_streaming() {
200        let candles: Vec<Candle> = (0..80_i64)
201            .map(|i| {
202                let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
203                candle(base + 1.0, base - 1.0, i)
204            })
205            .collect();
206        let mut a = Alligator::classic();
207        let mut b = Alligator::classic();
208        assert_eq!(
209            a.batch(&candles),
210            candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
211        );
212    }
213
214    #[test]
215    fn reset_clears_state() {
216        let mut alligator = Alligator::classic();
217        let candles: Vec<Candle> = (0..40).map(|i| candle(11.0, 9.0, i)).collect();
218        alligator.batch(&candles);
219        assert!(alligator.is_ready());
220        alligator.reset();
221        assert!(!alligator.is_ready());
222    }
223}