1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
51pub struct RisingThreeMethods {
52 c1: Option<Candle>,
53 c2: Option<Candle>,
54 c3: Option<Candle>,
55 c4: Option<Candle>,
56 has_emitted: bool,
57}
58
59impl RisingThreeMethods {
60 pub const fn new() -> Self {
62 Self {
63 c1: None,
64 c2: None,
65 c3: None,
66 c4: None,
67 has_emitted: false,
68 }
69 }
70}
71
72impl Indicator for RisingThreeMethods {
73 type Input = Candle;
74 type Output = f64;
75
76 #[inline]
77 fn update(&mut self, candle: Candle) -> Option<f64> {
78 let bar1 = self.c1;
79 let bar2 = self.c2;
80 let bar3 = self.c3;
81 let bar4 = self.c4;
82 self.c1 = self.c2;
83 self.c2 = self.c3;
84 self.c3 = self.c4;
85 self.c4 = Some(candle);
86 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
87 return None;
88 };
89 self.has_emitted = true;
90 let range1 = bar1.high - bar1.low;
91 if range1 <= 0.0 {
92 return Some(0.0);
93 }
94 let body1 = bar1.close - bar1.open;
95 if body1 < 0.5 * range1 {
96 return Some(0.0); }
98 for mid in [bar2, bar3, bar4] {
101 let body = mid.open - mid.close;
102 if body <= 0.0
103 || body > 0.5 * body1
104 || mid.open.min(mid.close) >= bar1.high
105 || mid.open.max(mid.close) <= bar1.low
106 {
107 return Some(0.0);
108 }
109 }
110 if bar3.close >= bar2.close || bar4.close >= bar3.close {
112 return Some(0.0);
113 }
114 let body5 = candle.close - candle.open;
117 if body5 > 0.0
118 && body5 >= 0.5 * (candle.high - candle.low)
119 && candle.open > bar4.close
120 && candle.close > bar1.close
121 {
122 return Some(1.0);
123 }
124 Some(0.0)
125 }
126
127 fn reset(&mut self) {
128 self.c1 = None;
129 self.c2 = None;
130 self.c3 = None;
131 self.c4 = None;
132 self.has_emitted = false;
133 }
134
135 #[inline]
136 fn warmup_period(&self) -> usize {
137 5
138 }
139
140 #[inline]
141 fn is_ready(&self) -> bool {
142 self.has_emitted
143 }
144
145 #[inline]
146 fn name(&self) -> &'static str {
147 "RisingThreeMethods"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::BatchExt;
155
156 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
157 Candle::new(open, high, low, close, 1.0, ts).unwrap()
158 }
159
160 #[test]
161 fn accessors_and_metadata() {
162 let t = RisingThreeMethods::new();
163 assert_eq!(t.name(), "RisingThreeMethods");
164 assert_eq!(t.warmup_period(), 5);
165 assert!(!t.is_ready());
166 }
167
168 #[test]
169 fn rising_three_methods_is_plus_one() {
170 let mut t = RisingThreeMethods::new();
171 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
172 assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 1)), None);
173 assert_eq!(t.update(c(13.5, 13.6, 12.4, 12.5, 2)), None);
174 assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 3)), None);
175 assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(1.0));
176 }
177
178 #[test]
179 fn middle_bar_breaks_range_yields_zero() {
180 let mut t = RisingThreeMethods::new();
181 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
182 t.update(c(14.0, 14.1, 12.9, 13.0, 1));
183 t.update(c(15.5, 15.6, 15.1, 15.2, 2));
185 t.update(c(13.0, 13.1, 11.9, 12.0, 3));
186 assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0));
187 }
188
189 #[test]
190 fn bar5_not_new_high_yields_zero() {
191 let mut t = RisingThreeMethods::new();
192 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
193 t.update(c(14.0, 14.1, 12.9, 13.0, 1));
194 t.update(c(13.5, 13.6, 12.4, 12.5, 2));
195 t.update(c(13.0, 13.1, 11.9, 12.0, 3));
196 assert_eq!(t.update(c(12.5, 14.6, 12.4, 14.5, 4)), Some(0.0));
198 }
199
200 #[test]
201 fn first_four_bars_return_zero() {
202 let mut t = RisingThreeMethods::new();
203 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
204 assert_eq!(t.update(c(14.0, 14.1, 12.9, 13.0, 1)), None);
205 assert_eq!(t.update(c(13.5, 13.6, 12.4, 12.5, 2)), None);
206 assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 3)), None);
207 }
208
209 #[test]
210 fn batch_equals_streaming() {
211 let candles: Vec<Candle> = (0..40)
212 .map(|i| {
213 let base = 100.0 + i as f64;
214 c(base, base + 5.2, base - 0.1, base + 5.0, i)
215 })
216 .collect();
217 let mut a = RisingThreeMethods::new();
218 let mut b = RisingThreeMethods::new();
219 assert_eq!(
220 a.batch(&candles),
221 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
222 );
223 }
224
225 #[test]
226 fn reset_clears_state() {
227 let mut t = RisingThreeMethods::new();
228 t.update(c(10.0, 15.1, 9.9, 15.0, 0));
229 t.update(c(14.0, 14.1, 12.9, 13.0, 1));
230 t.update(c(13.5, 13.6, 12.4, 12.5, 2));
231 t.update(c(13.0, 13.1, 11.9, 12.0, 3));
232 t.update(c(12.5, 16.1, 12.4, 16.0, 4));
233 assert!(t.is_ready());
234 t.reset();
235 assert!(!t.is_ready());
236 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 0)), None);
237 }
238
239 #[test]
240 fn zero_range_first_bar_yields_zero() {
241 let mut t = RisingThreeMethods::new();
242 t.update(c(10.0, 10.0, 10.0, 10.0, 0));
244 t.update(c(14.0, 14.1, 12.9, 13.0, 1));
245 t.update(c(13.5, 13.6, 12.4, 12.5, 2));
246 t.update(c(13.0, 13.1, 11.9, 12.0, 3));
247 assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0));
248 }
249
250 #[test]
251 fn short_first_body_yields_zero() {
252 let mut t = RisingThreeMethods::new();
253 t.update(c(10.0, 16.0, 9.0, 10.2, 0));
255 t.update(c(14.0, 14.1, 12.9, 13.0, 1));
256 t.update(c(13.5, 13.6, 12.4, 12.5, 2));
257 t.update(c(13.0, 13.1, 11.9, 12.0, 3));
258 assert_eq!(t.update(c(12.5, 16.1, 12.4, 16.0, 4)), Some(0.0));
259 }
260
261 fn base() -> [Candle; 5] {
264 [
265 c(10.0, 15.1, 9.9, 15.0, 0),
266 c(14.0, 14.1, 12.9, 13.0, 1),
267 c(13.5, 13.6, 12.4, 12.5, 2),
268 c(13.0, 13.1, 11.9, 12.0, 3),
269 c(12.5, 16.1, 12.4, 16.0, 4),
270 ]
271 }
272
273 fn run(bars: [Candle; 5]) -> Option<f64> {
274 let mut t = RisingThreeMethods::new();
275 bars.iter().map(|b| t.update(*b)).last().unwrap()
276 }
277
278 fn with(index: usize, candle: Candle) -> [Candle; 5] {
279 let mut bars = base();
280 bars[index] = candle;
281 bars
282 }
283
284 #[test]
285 fn hand_computed_pattern() {
286 assert_eq!(run(base()), Some(1.0));
291 }
292
293 #[test]
294 fn middle_shadow_outside_range_but_body_overlapping_fires() {
295 assert_eq!(run(with(1, c(14.0, 15.5, 12.9, 13.0, 1))), Some(1.0));
297 assert_eq!(run(with(1, c(15.4, 15.5, 14.8, 14.9, 1))), Some(1.0));
299 assert_eq!(run(with(3, c(10.4, 10.5, 9.0, 10.0, 3))), Some(1.0));
301 }
302
303 #[test]
304 fn middle_body_fully_outside_range_yields_zero() {
305 assert_eq!(run(with(1, c(15.6, 15.7, 15.0, 15.1, 1))), Some(0.0));
307 assert_eq!(run(with(3, c(9.8, 9.9, 9.0, 9.2, 3))), Some(0.0));
309 assert_eq!(run(with(3, c(9.9, 10.0, 9.0, 9.5, 3))), Some(0.0));
311 }
312
313 #[test]
314 fn middle_body_colour_and_size_rules() {
315 assert_eq!(run(with(2, c(12.5, 13.6, 12.4, 13.5, 2))), Some(0.0));
317 assert_eq!(run(with(2, c(12.5, 13.6, 12.4, 12.5, 2))), Some(0.0));
319 assert_eq!(run(with(1, c(14.9, 15.0, 11.9, 12.0, 1))), Some(0.0));
321 }
322
323 #[test]
324 fn closes_not_drifting_down_yields_zero() {
325 assert_eq!(run(with(2, c(13.6, 13.7, 13.0, 13.1, 2))), Some(0.0));
327 assert_eq!(run(with(2, c(13.6, 13.7, 12.9, 13.0, 2))), Some(0.0));
329 assert_eq!(run(with(3, c(13.0, 13.1, 12.5, 12.6, 3))), Some(0.0));
331 }
332
333 #[test]
334 fn fifth_bar_conditions() {
335 assert_eq!(run(with(4, c(16.0, 16.1, 12.4, 15.5, 4))), Some(0.0));
337 assert_eq!(run(with(4, c(16.0, 16.1, 12.4, 16.0, 4))), Some(0.0));
339 assert_eq!(run(with(4, c(12.5, 20.0, 12.0, 16.0, 4))), Some(0.0));
341 assert_eq!(run(with(4, c(12.0, 16.1, 11.9, 16.0, 4))), Some(0.0));
343 assert_eq!(run(with(4, c(12.5, 15.1, 12.4, 15.0, 4))), Some(0.0));
345 }
346
347 #[test]
348 fn first_value_lands_exactly_at_warmup_index() {
349 let mut t = RisingThreeMethods::new();
350 let out = t.batch(&base());
351 let warm = t.warmup_period();
352 assert!(out[..warm - 1].iter().all(Option::is_none));
353 assert_eq!(out[warm - 1], Some(1.0));
354 }
355
356 fn mixed_series() -> Vec<Candle> {
357 base().iter().cycle().take(30).copied().collect()
358 }
359
360 #[test]
361 fn reset_replays_identically() {
362 let candles = mixed_series();
363 let fresh = RisingThreeMethods::new().batch(&candles);
364 let mut t = RisingThreeMethods::new();
365 let _ = t.batch(&candles);
366 t.reset();
367 assert_eq!(t.batch(&candles), fresh);
368 }
369
370 #[test]
371 fn batch_nan_into_matches_streaming_bits() {
372 let candles = mixed_series();
373 let mut t = RisingThreeMethods::new();
374 let streamed: Vec<f64> = candles
375 .iter()
376 .map(|x| t.update(*x).unwrap_or(f64::NAN))
377 .collect();
378 let mut out = vec![0.0; candles.len()];
379 RisingThreeMethods::new().batch_nan_into(&candles, &mut out);
380 assert!(streamed
381 .iter()
382 .zip(&out)
383 .all(|(a, b)| a.to_bits() == b.to_bits()));
384 assert!(streamed.contains(&1.0));
385 }
386}