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 self.has_emitted = true;
86 let bar1 = self.c1;
87 let bar2 = self.c2;
88 let bar3 = self.c3;
89 let bar4 = self.c4;
90 self.c1 = self.c2;
91 self.c2 = self.c3;
92 self.c3 = self.c4;
93 self.c4 = Some(candle);
94 let (Some(bar1), Some(bar2), Some(bar3), Some(bar4)) = (bar1, bar2, bar3, bar4) else {
95 return Some(0.0);
96 };
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 fn warmup_period(&self) -> usize {
141 5
142 }
143
144 fn is_ready(&self) -> bool {
145 self.has_emitted
146 }
147
148 fn name(&self) -> &'static str {
149 "Breakaway"
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 accessors_and_metadata() {
164 let t = Breakaway::new();
165 assert_eq!(t.name(), "Breakaway");
166 assert_eq!(t.warmup_period(), 5);
167 assert!(!t.is_ready());
168 }
169
170 #[test]
171 fn bullish_breakaway_is_plus_one() {
172 let mut t = Breakaway::new();
173 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
174 assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), Some(0.0));
175 assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), Some(0.0));
176 assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), Some(0.0));
177 assert_eq!(t.update(c(9.5, 14.7, 9.4, 14.5, 4)), Some(1.0));
178 }
179
180 #[test]
181 fn bearish_breakaway_is_minus_one() {
182 let mut t = Breakaway::new();
183 assert_eq!(t.update(c(15.0, 20.2, 14.8, 20.0, 0)), Some(0.0));
184 assert_eq!(t.update(c(21.0, 23.1, 20.9, 23.0, 1)), Some(0.0));
185 assert_eq!(t.update(c(22.5, 24.5, 21.5, 24.0, 2)), Some(0.0));
186 assert_eq!(t.update(c(24.0, 26.5, 23.0, 26.0, 3)), Some(0.0));
187 assert_eq!(t.update(c(27.0, 27.2, 20.4, 20.5, 4)), Some(-1.0));
188 }
189
190 #[test]
191 fn no_body_gap_yields_zero() {
192 let mut t = Breakaway::new();
193 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
195 t.update(c(16.0, 16.1, 13.9, 14.0, 1));
196 t.update(c(13.5, 14.0, 11.5, 12.0, 2));
197 t.update(c(12.0, 12.5, 10.0, 10.5, 3));
198 assert_eq!(t.update(c(10.5, 15.7, 10.4, 15.5, 4)), Some(0.0));
199 }
200
201 #[test]
202 fn bullish_close_outside_gap_yields_zero() {
203 let mut t = Breakaway::new();
204 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
205 t.update(c(14.0, 14.1, 11.9, 12.0, 1));
206 t.update(c(12.5, 13.0, 10.5, 11.0, 2));
207 t.update(c(11.0, 11.5, 9.0, 9.5, 3));
208 assert_eq!(t.update(c(9.5, 13.2, 9.4, 13.0, 4)), Some(0.0));
210 }
211
212 #[test]
213 fn first_four_bars_return_zero() {
214 let mut t = Breakaway::new();
215 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
216 assert_eq!(t.update(c(14.0, 14.1, 11.9, 12.0, 1)), Some(0.0));
217 assert_eq!(t.update(c(12.5, 13.0, 10.5, 11.0, 2)), Some(0.0));
218 assert_eq!(t.update(c(11.0, 11.5, 9.0, 9.5, 3)), Some(0.0));
219 }
220
221 #[test]
222 fn batch_equals_streaming() {
223 let candles: Vec<Candle> = (0..40)
224 .map(|i| {
225 let base = 100.0 + i as f64;
226 c(base, base + 2.0, base - 0.5, base + 1.5, i)
227 })
228 .collect();
229 let mut a = Breakaway::new();
230 let mut b = Breakaway::new();
231 assert_eq!(
232 a.batch(&candles),
233 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
234 );
235 }
236
237 #[test]
238 fn reset_clears_state() {
239 let mut t = Breakaway::new();
240 t.update(c(20.0, 20.2, 14.8, 15.0, 0));
241 t.update(c(14.0, 14.1, 11.9, 12.0, 1));
242 t.update(c(12.5, 13.0, 10.5, 11.0, 2));
243 t.update(c(11.0, 11.5, 9.0, 9.5, 3));
244 t.update(c(9.5, 14.7, 9.4, 14.5, 4));
245 assert!(t.is_ready());
246 t.reset();
247 assert!(!t.is_ready());
248 assert_eq!(t.update(c(20.0, 20.2, 14.8, 15.0, 0)), Some(0.0));
249 }
250}