1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
44pub struct LadderBottom {
45 c1: Option<Candle>,
46 c2: Option<Candle>,
47 c3: Option<Candle>,
48 c4: Option<Candle>,
49 has_emitted: bool,
50}
51
52impl LadderBottom {
53 pub const fn new() -> Self {
55 Self {
56 c1: None,
57 c2: None,
58 c3: None,
59 c4: None,
60 has_emitted: false,
61 }
62 }
63}
64
65impl Indicator for LadderBottom {
66 type Input = Candle;
67 type Output = f64;
68
69 fn update(&mut self, candle: Candle) -> Option<f64> {
70 self.has_emitted = true;
71 let bar1 = self.c1;
72 let bar2 = self.c2;
73 let bar3 = self.c3;
74 let bar4 = self.c4;
75 self.c1 = self.c2;
76 self.c2 = self.c3;
77 self.c3 = self.c4;
78 self.c4 = Some(candle);
79 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
80 return Some(0.0);
81 };
82 if bar1.close < bar1.open
83 && bar2.close < bar2.open
84 && bar3.close < bar3.open
85 && bar2.open < bar1.open
86 && bar2.close < bar1.close
87 && bar3.open < bar2.open
88 && bar3.close < bar2.close
89 && bar4.close < bar4.open
90 && bar4.high > bar4.open
91 && candle.close > candle.open
92 && candle.open > bar4.open
93 {
94 return Some(1.0);
95 }
96 Some(0.0)
97 }
98
99 fn reset(&mut self) {
100 self.c1 = None;
101 self.c2 = None;
102 self.c3 = None;
103 self.c4 = None;
104 self.has_emitted = false;
105 }
106
107 fn warmup_period(&self) -> usize {
108 5
109 }
110
111 fn is_ready(&self) -> bool {
112 self.has_emitted
113 }
114
115 fn name(&self) -> &'static str {
116 "LadderBottom"
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::traits::BatchExt;
124
125 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
126 Candle::new(open, high, low, close, 1.0, ts).unwrap()
127 }
128
129 #[test]
130 fn accessors_and_metadata() {
131 let t = LadderBottom::new();
132 assert_eq!(t.name(), "LadderBottom");
133 assert_eq!(t.warmup_period(), 5);
134 assert!(!t.is_ready());
135 }
136
137 #[test]
138 fn ladder_bottom_is_plus_one() {
139 let mut t = LadderBottom::new();
140 assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
141 assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
142 assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
143 assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
144 assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(1.0));
145 }
146
147 #[test]
148 fn fourth_bar_without_upper_shadow_yields_zero() {
149 let mut t = LadderBottom::new();
150 t.update(c(20.0, 20.1, 17.9, 18.0, 0));
151 t.update(c(18.0, 18.1, 15.9, 16.0, 1));
152 t.update(c(16.0, 16.1, 13.9, 14.0, 2));
153 t.update(c(14.0, 14.0, 12.4, 12.5, 3));
155 assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
156 }
157
158 #[test]
159 fn not_three_descending_blacks_yields_zero() {
160 let mut t = LadderBottom::new();
161 t.update(c(20.0, 20.1, 17.9, 18.0, 0));
163 t.update(c(21.0, 21.1, 18.9, 19.0, 1));
164 t.update(c(16.0, 16.1, 13.9, 14.0, 2));
165 t.update(c(14.0, 15.0, 12.4, 12.5, 3));
166 assert_eq!(t.update(c(15.0, 17.1, 14.9, 17.0, 4)), Some(0.0));
167 }
168
169 #[test]
170 fn first_four_bars_return_zero() {
171 let mut t = LadderBottom::new();
172 assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
173 assert_eq!(t.update(c(18.0, 18.1, 15.9, 16.0, 1)), Some(0.0));
174 assert_eq!(t.update(c(16.0, 16.1, 13.9, 14.0, 2)), Some(0.0));
175 assert_eq!(t.update(c(14.0, 15.0, 12.4, 12.5, 3)), Some(0.0));
176 }
177
178 #[test]
179 fn batch_equals_streaming() {
180 let candles: Vec<Candle> = (0..40)
181 .map(|i| {
182 let base = 200.0 - i as f64;
183 c(base, base + 0.1, base - 2.1, base - 2.0, i)
184 })
185 .collect();
186 let mut a = LadderBottom::new();
187 let mut b = LadderBottom::new();
188 assert_eq!(
189 a.batch(&candles),
190 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
191 );
192 }
193
194 #[test]
195 fn reset_clears_state() {
196 let mut t = LadderBottom::new();
197 t.update(c(20.0, 20.1, 17.9, 18.0, 0));
198 t.update(c(18.0, 18.1, 15.9, 16.0, 1));
199 t.update(c(16.0, 16.1, 13.9, 14.0, 2));
200 t.update(c(14.0, 15.0, 12.4, 12.5, 3));
201 t.update(c(15.0, 17.1, 14.9, 17.0, 4));
202 assert!(t.is_ready());
203 t.reset();
204 assert!(!t.is_ready());
205 assert_eq!(t.update(c(20.0, 20.1, 17.9, 18.0, 0)), Some(0.0));
206 }
207}