wickra_core/indicators/
alligator.rs1use crate::error::{Error, Result};
4use crate::indicators::smma::Smma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct AlligatorOutput {
12 pub jaw: f64,
14 pub teeth: f64,
16 pub lips: f64,
18}
19
20#[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 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 pub fn classic() -> Self {
75 Self::new(13, 8, 5).expect("classic Alligator parameters are valid")
76 }
77
78 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 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 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 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 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}