1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
46pub struct AbandonedBaby {
47 tolerance: f64,
48 prev: Option<Candle>,
49 prev_prev: Option<Candle>,
50 has_emitted: bool,
51}
52
53impl Default for AbandonedBaby {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl AbandonedBaby {
60 pub const fn new() -> Self {
62 Self {
63 tolerance: 0.001,
64 prev: None,
65 prev_prev: None,
66 has_emitted: false,
67 }
68 }
69
70 pub fn with_tolerance(tolerance: f64) -> Result<Self> {
74 if !(0.0..1.0).contains(&tolerance) {
75 return Err(Error::InvalidPeriod {
76 message: "abandoned baby tolerance must lie in [0, 1)",
77 });
78 }
79 Ok(Self {
80 tolerance,
81 prev: None,
82 prev_prev: None,
83 has_emitted: false,
84 })
85 }
86
87 pub fn tolerance(&self) -> f64 {
89 self.tolerance
90 }
91}
92
93impl Indicator for AbandonedBaby {
94 type Input = Candle;
95 type Output = f64;
96
97 #[inline]
98 fn update(&mut self, candle: Candle) -> Option<f64> {
99 let pp = self.prev_prev;
100 let p = self.prev;
101 self.prev_prev = self.prev;
102 self.prev = Some(candle);
103 let (Some(bar1), Some(bar2)) = (pp, p) else {
104 return None;
105 };
106 self.has_emitted = true;
107 let tol = self.tolerance * bar2.open.abs().max(bar2.close.abs());
108 let bar2_is_doji = (bar2.close - bar2.open).abs() <= tol;
109 if !bar2_is_doji {
110 return Some(0.0);
111 }
112 if bar1.close < bar1.open
114 && bar2.high < bar1.low
115 && candle.close > candle.open
116 && candle.low > bar2.high
117 {
118 return Some(1.0);
119 }
120 if bar1.close > bar1.open
122 && bar2.low > bar1.high
123 && candle.close < candle.open
124 && candle.high < bar2.low
125 {
126 return Some(-1.0);
127 }
128 Some(0.0)
129 }
130
131 fn reset(&mut self) {
132 self.prev = None;
133 self.prev_prev = None;
134 self.has_emitted = false;
135 }
136
137 #[inline]
138 fn warmup_period(&self) -> usize {
139 3
140 }
141
142 #[inline]
143 fn is_ready(&self) -> bool {
144 self.has_emitted
145 }
146
147 #[inline]
148 fn name(&self) -> &'static str {
149 "AbandonedBaby"
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::traits::BatchExt;
157
158 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
159 Candle::new(open, high, low, close, 1.0, ts).unwrap()
160 }
161
162 #[test]
163 fn rejects_invalid_tolerance() {
164 assert!(AbandonedBaby::with_tolerance(-0.01).is_err());
165 assert!(AbandonedBaby::with_tolerance(1.0).is_err());
166 }
167
168 #[test]
169 fn accepts_valid_tolerance() {
170 let t = AbandonedBaby::with_tolerance(0.0).unwrap();
171 assert!((t.tolerance() - 0.0).abs() < 1e-12);
172 }
173
174 #[test]
175 fn accessors_and_metadata() {
176 let t = AbandonedBaby::default();
177 assert_eq!(t.name(), "AbandonedBaby");
178 assert_eq!(t.warmup_period(), 3);
179 assert!(!t.is_ready());
180 assert!((t.tolerance() - 0.001).abs() < 1e-12);
181 }
182
183 #[test]
184 fn bullish_abandoned_baby_is_plus_one() {
185 let mut t = AbandonedBaby::new();
186 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
187 assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), None);
188 assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(1.0));
189 }
190
191 #[test]
192 fn bearish_abandoned_baby_is_minus_one() {
193 let mut t = AbandonedBaby::new();
194 assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), None);
195 assert_eq!(t.update(c(22.0, 22.1, 21.9, 22.0, 1)), None);
196 assert_eq!(t.update(c(19.0, 19.1, 16.9, 17.0, 2)), Some(-1.0));
197 }
198
199 #[test]
200 fn middle_not_doji_yields_zero() {
201 let mut t = AbandonedBaby::new();
202 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
203 assert_eq!(t.update(c(13.0, 14.0, 11.0, 11.5, 1)), None);
205 assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(0.0));
206 }
207
208 #[test]
209 fn no_gap_yields_zero() {
210 let mut t = AbandonedBaby::new();
211 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
212 assert_eq!(t.update(c(15.0, 15.1, 14.9, 15.0, 1)), None);
214 assert_eq!(t.update(c(16.0, 18.1, 15.9, 18.0, 2)), Some(0.0));
215 }
216
217 #[test]
218 fn first_two_bars_return_zero() {
219 let mut t = AbandonedBaby::new();
220 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
221 assert_eq!(t.update(c(13.0, 13.1, 12.9, 13.0, 1)), None);
222 }
223
224 #[test]
225 fn batch_equals_streaming() {
226 let candles: Vec<Candle> = (0..40)
227 .map(|i| {
228 let base = 100.0 + (i as f64 * 0.3).sin() * 5.0;
229 c(base, base + 1.0, base - 1.0, base + 0.5, i)
230 })
231 .collect();
232 let mut a = AbandonedBaby::new();
233 let mut b = AbandonedBaby::new();
234 assert_eq!(
235 a.batch(&candles),
236 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
237 );
238 }
239
240 #[test]
241 fn reset_clears_state() {
242 let mut t = AbandonedBaby::new();
243 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
244 t.update(c(13.0, 13.1, 12.9, 13.0, 1));
245 t.update(c(16.0, 18.1, 15.9, 18.0, 2));
246 assert!(t.is_ready());
247 t.reset();
248 assert!(!t.is_ready());
249 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
250 }
251}