1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
59pub struct Breakaway {
60 c1: Option<Candle>,
61 c2: Option<Candle>,
62 c3: Option<Candle>,
63 c4: Option<Candle>,
64 has_emitted: bool,
65}
66
67impl Breakaway {
68 pub const fn new() -> Self {
70 Self {
71 c1: None,
72 c2: None,
73 c3: None,
74 c4: None,
75 has_emitted: false,
76 }
77 }
78}
79
80impl Indicator for Breakaway {
81 type Input = Candle;
82 type Output = f64;
83
84 fn update(&mut self, candle: Candle) -> Option<f64> {
85 let bar1 = self.c1;
86 let bar2 = self.c2;
87 let bar3 = self.c3;
88 let bar4 = self.c4;
89 self.c1 = self.c2;
90 self.c2 = self.c3;
91 self.c3 = self.c4;
92 self.c4 = Some(candle);
93 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
94 return None;
95 };
96 self.has_emitted = true;
97 if bar1.close < bar1.open
100 && bar2.close < bar2.open
101 && bar2.open < bar1.close
102 && bar3.high < bar2.high
103 && bar3.low < bar2.low
104 && bar4.close < bar4.open
105 && bar4.high < bar3.high
106 && bar4.low < bar3.low
107 && candle.close > candle.open
108 && candle.close > bar2.open
109 && candle.close < bar1.close
110 {
111 return Some(1.0);
112 }
113 if bar1.close > bar1.open
116 && bar2.close > bar2.open
117 && bar2.open > bar1.close
118 && bar3.high > bar2.high
119 && bar3.low > bar2.low
120 && bar4.close > bar4.open
121 && bar4.high > bar3.high
122 && bar4.low > bar3.low
123 && candle.close < candle.open
124 && candle.close < bar2.open
125 && candle.close > bar1.close
126 {
127 return Some(-1.0);
128 }
129 Some(0.0)
130 }
131
132 fn reset(&mut self) {
133 self.c1 = None;
134 self.c2 = None;
135 self.c3 = None;
136 self.c4 = None;
137 self.has_emitted = false;
138 }
139
140 #[inline]
141 fn warmup_period(&self) -> usize {
142 5
143 }
144
145 #[inline]
146 fn is_ready(&self) -> bool {
147 self.has_emitted
148 }
149
150 #[inline]
151 fn name(&self) -> &'static str {
152 "Breakaway"
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::traits::BatchExt;
160
161 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
162 Candle::new(open, high, low, close, 1.0, ts).unwrap()
163 }
164
165 #[test]
166 fn accessors_and_metadata() {
167 let t = Breakaway::new();
168 assert_eq!(t.name(), "Breakaway");
169 assert_eq!(t.warmup_period(), 5);
170 assert!(!t.is_ready());
171 }
172
173 #[test]
174 fn bullish_breakaway_is_plus_one() {
175 let mut t = Breakaway::new();
176 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), None);
177 assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), None);
178 assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), None);
179 assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), None);
180 assert_eq!(t.update(c(9.5, 14.7, 9.4, 14.5, 4)), Some(1.0));
181 }
182
183 #[test]
184 fn bearish_breakaway_is_minus_one() {
185 let mut t = Breakaway::new();
186 assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), None);
187 assert_eq!(t.update(c(21.0, 23.1, 20.9, 23.0, 1)), None);
188 assert_eq!(t.update(c(22.5, 24.5, 21.5, 24.0, 2)), None);
189 assert_eq!(t.update(c(24.0, 26.5, 23.0, 26.0, 3)), None);
190 assert_eq!(t.update(c(27.0, 27.2, 20.4, 20.5, 4)), Some(-1.0));
191 }
192
193 #[test]
194 fn no_body_gap_yields_zero() {
195 let mut t = Breakaway::new();
196 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
198 t.update(c(16.0, 16.1, 13.9, 14.0, 1));
199 t.update(c(13.5, 14.0, 11.5, 12.0, 2));
200 t.update(c(12.0, 12.5, 10.0, 10.5, 3));
201 assert_eq!(t.update(c(10.5, 15.7, 10.4, 15.5, 4)), Some(0.0));
202 }
203
204 #[test]
205 fn bullish_close_outside_gap_yields_zero() {
206 let mut t = Breakaway::new();
207 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
208 t.update(c(14.0, 14.1, 11.9, 12.0, 1));
209 t.update(c(12.5, 13.0, 10.5, 11.0, 2));
210 t.update(c(11.0, 11.5, 9.0, 9.5, 3));
211 assert_eq!(t.update(c(9.5, 13.2, 9.4, 13.0, 4)), Some(0.0));
213 }
214
215 #[test]
216 fn first_four_bars_return_zero() {
217 let mut t = Breakaway::new();
218 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), None);
219 assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), None);
220 assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), None);
221 assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), 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;
229 c(base, base + 2.0, base - 0.5, base + 1.5, i)
230 })
231 .collect();
232 let mut a = Breakaway::new();
233 let mut b = Breakaway::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 = Breakaway::new();
243 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
244 t.update(c(14.0, 14.1, 11.9, 12.0, 1));
245 t.update(c(12.5, 13.0, 10.5, 11.0, 2));
246 t.update(c(11.0, 11.5, 9.0, 9.5, 3));
247 t.update(c(9.5, 14.7, 9.4, 14.5, 4));
248 assert!(t.is_ready());
249 t.reset();
250 assert!(!t.is_ready());
251 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), None);
252 }
253}