1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
46pub struct LadderBottom {
47 c1: Option<Candle>,
48 c2: Option<Candle>,
49 c3: Option<Candle>,
50 c4: Option<Candle>,
51 has_emitted: bool,
52}
53
54impl LadderBottom {
55 pub const fn new() -> Self {
57 Self {
58 c1: None,
59 c2: None,
60 c3: None,
61 c4: None,
62 has_emitted: false,
63 }
64 }
65}
66
67impl Indicator for LadderBottom {
68 type Input = Candle;
69 type Output = f64;
70
71 #[inline]
72 fn update(&mut self, candle: Candle) -> Option<f64> {
73 let bar1 = self.c1;
74 let bar2 = self.c2;
75 let bar3 = self.c3;
76 let bar4 = self.c4;
77 self.c1 = self.c2;
78 self.c2 = self.c3;
79 self.c3 = self.c4;
80 self.c4 = Some(candle);
81 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
82 return None;
83 };
84 self.has_emitted = true;
85 if bar1.close < bar1.open
86 && bar2.close < bar2.open
87 && bar3.close < bar3.open
88 && bar2.open < bar1.open
89 && bar2.close < bar1.close
90 && bar3.open < bar2.open
91 && bar3.close < bar2.close
92 && bar4.close < bar4.open
93 && bar4.high > bar4.open
94 && candle.close > candle.open
95 && candle.open > bar4.open
96 && candle.close > bar4.high
97 {
98 return Some(1.0);
99 }
100 Some(0.0)
101 }
102
103 fn reset(&mut self) {
104 self.c1 = None;
105 self.c2 = None;
106 self.c3 = None;
107 self.c4 = None;
108 self.has_emitted = false;
109 }
110
111 #[inline]
112 fn warmup_period(&self) -> usize {
113 5
114 }
115
116 #[inline]
117 fn is_ready(&self) -> bool {
118 self.has_emitted
119 }
120
121 #[inline]
122 fn name(&self) -> &'static str {
123 "LadderBottom"
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::traits::BatchExt;
131
132 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
133 Candle::new(open, high, low, close, 1.0, ts).unwrap()
134 }
135
136 #[test]
137 fn accessors_and_metadata() {
138 let t = LadderBottom::new();
139 assert_eq!(t.name(), "LadderBottom");
140 assert_eq!(t.warmup_period(), 5);
141 assert!(!t.is_ready());
142 }
143
144 #[test]
145 fn ladder_bottom_is_plus_one() {
146 let mut t = LadderBottom::new();
147 assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), None);
148 assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), None);
149 assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), None);
150 assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), None);
151 assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(1.0));
152 }
153
154 #[test]
155 fn fourth_bar_without_upper_shadow_yields_zero() {
156 let mut t = LadderBottom::new();
157 t.update(c(20.0, 20.1, 17.9, 18.0, 0));
158 t.update(c(18.0, 18.1, 15.9, 16.0, 1));
159 t.update(c(16.0, 16.1, 13.9, 14.0, 2));
160 t.update(c(14.0, 14.0, 12.4, 12.5, 3));
162 assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
163 }
164
165 #[test]
166 fn not_three_descending_blacks_yields_zero() {
167 let mut t = LadderBottom::new();
168 t.update(c(20.0, 20.1, 17.9, 18.0, 0));
170 t.update(c(21.0, 21.1, 18.9, 19.0, 1));
171 t.update(c(16.0, 16.1, 13.9, 14.0, 2));
172 t.update(c(14.0, 15.0, 12.4, 12.5, 3));
173 assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
174 }
175
176 #[test]
177 fn first_four_bars_return_zero() {
178 let mut t = LadderBottom::new();
179 assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), None);
180 assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), None);
181 assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), None);
182 assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), None);
183 }
184
185 #[test]
186 fn batch_equals_streaming() {
187 let candles: Vec<Candle> = (0..40)
188 .map(|i| {
189 let base = 200.0 - i as f64;
190 c(base, base + 0.1, base - 2.1, base - 2.0, i)
191 })
192 .collect();
193 let mut a = LadderBottom::new();
194 let mut b = LadderBottom::new();
195 assert_eq!(
196 a.batch(&candles),
197 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
198 );
199 }
200
201 #[test]
202 fn reset_clears_state() {
203 let mut t = LadderBottom::new();
204 t.update(c(20.0, 20.1, 17.9, 18.0, 0));
205 t.update(c(18.0, 18.1, 15.9, 16.0, 1));
206 t.update(c(16.0, 16.1, 13.9, 14.0, 2));
207 t.update(c(14.0, 15.0, 12.4, 12.5, 3));
208 t.update(c(15.0, 17.1, 14.9, 17.0, 4));
209 assert!(t.is_ready());
210 t.reset();
211 assert!(!t.is_ready());
212 assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), None);
213 }
214
215 fn base() -> [Candle; 5] {
217 [
218 c(20.0, 20.1, 17.9, 18.0, 0),
219 c(18.0, 18.1, 15.9, 16.0, 1),
220 c(16.0, 16.1, 13.9, 14.0, 2),
221 c(14.0, 15.0, 12.4, 12.5, 3),
222 c(15.0, 17.1, 14.9, 17.0, 4),
223 ]
224 }
225
226 fn run(bars: [Candle; 5]) -> Option<f64> {
227 let mut t = LadderBottom::new();
228 bars.iter().map(|b| t.update(*b)).last().unwrap()
229 }
230
231 fn with(index: usize, candle: Candle) -> [Candle; 5] {
232 let mut bars = base();
233 bars[index] = candle;
234 bars
235 }
236
237 #[test]
238 fn close5_must_exceed_high4() {
239 assert_eq!(run(with(4, c(14.5, 15.2, 14.4, 15.0, 4))), Some(0.0));
242 assert_eq!(run(with(4, c(14.5, 15.2, 14.4, 15.05, 4))), Some(1.0));
244 }
245
246 #[test]
247 fn every_condition_is_required() {
248 assert_eq!(run(base()), Some(1.0));
249 assert_eq!(run(with(0, c(18.5, 20.1, 18.4, 19.0, 0))), Some(0.0));
251 assert_eq!(run(with(1, c(17.0, 18.1, 15.9, 17.5, 1))), Some(0.0));
253 assert_eq!(run(with(2, c(14.5, 15.5, 13.9, 15.0, 2))), Some(0.0));
255 assert_eq!(run(with(1, c(20.5, 20.6, 15.9, 16.0, 1))), Some(0.0));
257 assert_eq!(run(with(1, c(19.0, 19.1, 18.4, 18.5, 1))), Some(0.0));
259 assert_eq!(run(with(2, c(18.5, 18.6, 13.9, 14.0, 2))), Some(0.0));
261 assert_eq!(run(with(2, c(17.0, 17.1, 16.4, 16.5, 2))), Some(0.0));
263 assert_eq!(run(with(3, c(12.5, 15.0, 12.4, 14.0, 3))), Some(0.0));
265 assert_eq!(run(with(4, c(17.0, 17.1, 14.9, 16.0, 4))), Some(0.0));
267 assert_eq!(run(with(4, c(13.5, 17.1, 13.4, 17.0, 4))), Some(0.0));
269 }
270
271 #[test]
272 fn first_value_lands_exactly_at_warmup_index() {
273 let mut t = LadderBottom::new();
274 let out = t.batch(&base());
275 let warm = t.warmup_period();
276 assert!(out[..warm - 1].iter().all(Option::is_none));
277 assert_eq!(out[warm - 1], Some(1.0));
278 }
279
280 fn mixed_series() -> Vec<Candle> {
281 let mut candles: Vec<Candle> = Vec::new();
282 for round in 0..6 {
283 for (k, bar) in base().iter().enumerate() {
284 let ts = i64::try_from(round * 5 + k).unwrap();
285 candles.push(Candle::new(bar.open, bar.high, bar.low, bar.close, 1.0, ts).unwrap());
286 }
287 }
288 candles
289 }
290
291 #[test]
292 fn reset_replays_identically() {
293 let candles = mixed_series();
294 let fresh = LadderBottom::new().batch(&candles);
295 let mut t = LadderBottom::new();
296 let _ = t.batch(&candles);
297 t.reset();
298 assert_eq!(t.batch(&candles), fresh);
299 }
300
301 #[test]
302 fn batch_nan_into_matches_streaming_bits() {
303 let candles = mixed_series();
304 let mut t = LadderBottom::new();
305 let streamed: Vec<f64> = candles
306 .iter()
307 .map(|x| t.update(*x).unwrap_or(f64::NAN))
308 .collect();
309 let mut out = vec![0.0; candles.len()];
310 LadderBottom::new().batch_nan_into(&candles, &mut out);
311 assert!(streamed
312 .iter()
313 .zip(&out)
314 .all(|(a, b)| a.to_bits() == b.to_bits()));
315 assert!(streamed.contains(&1.0));
316 }
317}