1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
49pub struct Counterattack {
50 equal_tolerance: f64,
51 prev: Option<Candle>,
52 has_emitted: bool,
53}
54
55impl Default for Counterattack {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl Counterattack {
62 pub const fn new() -> Self {
64 Self {
65 equal_tolerance: 0.05,
66 prev: None,
67 has_emitted: false,
68 }
69 }
70
71 pub fn with_tolerance(equal_tolerance: f64) -> Result<Self> {
76 if !(0.0..1.0).contains(&equal_tolerance) {
77 return Err(Error::InvalidPeriod {
78 message: "counterattack equal tolerance must lie in [0, 1)",
79 });
80 }
81 Ok(Self {
82 equal_tolerance,
83 prev: None,
84 has_emitted: false,
85 })
86 }
87
88 pub fn equal_tolerance(&self) -> f64 {
90 self.equal_tolerance
91 }
92}
93
94impl Indicator for Counterattack {
95 type Input = Candle;
96 type Output = f64;
97
98 fn update(&mut self, candle: Candle) -> Option<f64> {
99 self.has_emitted = true;
100 let prev = self.prev;
101 self.prev = Some(candle);
102 let Some(bar1) = prev else {
103 return Some(0.0);
104 };
105 let range1 = bar1.high - bar1.low;
106 let range2 = candle.high - candle.low;
107 let body1 = bar1.close - bar1.open;
108 let body2 = candle.close - candle.open;
109 let long1 = body1.abs() >= 0.5 * range1;
110 let long2 = body2.abs() >= 0.5 * range2;
111 let tol = self.equal_tolerance * 0.5 * (range1 + range2);
112 let equal_close = (candle.close - bar1.close).abs() <= tol;
113 if !(long1 && long2 && equal_close) {
114 return Some(0.0);
115 }
116 if body1 < 0.0 && body2 > 0.0 {
118 return Some(1.0);
119 }
120 if body1 > 0.0 && body2 < 0.0 {
122 return Some(-1.0);
123 }
124 Some(0.0)
125 }
126
127 fn reset(&mut self) {
128 self.prev = None;
129 self.has_emitted = false;
130 }
131
132 fn warmup_period(&self) -> usize {
133 2
134 }
135
136 fn is_ready(&self) -> bool {
137 self.has_emitted
138 }
139
140 fn name(&self) -> &'static str {
141 "Counterattack"
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::traits::BatchExt;
149
150 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
151 Candle::new(open, high, low, close, 1.0, ts).unwrap()
152 }
153
154 #[test]
155 fn rejects_invalid_tolerance() {
156 assert!(Counterattack::with_tolerance(-0.01).is_err());
157 assert!(Counterattack::with_tolerance(1.0).is_err());
158 }
159
160 #[test]
161 fn accepts_valid_tolerance() {
162 let t = Counterattack::with_tolerance(0.0).unwrap();
163 assert!((t.equal_tolerance() - 0.0).abs() < 1e-12);
164 }
165
166 #[test]
167 fn accessors_and_metadata() {
168 let t = Counterattack::default();
169 assert_eq!(t.name(), "Counterattack");
170 assert_eq!(t.warmup_period(), 2);
171 assert!(!t.is_ready());
172 assert!((t.equal_tolerance() - 0.05).abs() < 1e-12);
173 }
174
175 #[test]
176 fn bullish_counterattack_is_plus_one() {
177 let mut t = Counterattack::new();
178 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
179 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 1)), Some(1.0));
180 }
181
182 #[test]
183 fn bearish_counterattack_is_minus_one() {
184 let mut t = Counterattack::new();
185 assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), Some(0.0));
186 assert_eq!(t.update(c(25.0, 25.1, 19.9, 20.0, 1)), Some(-1.0));
187 }
188
189 #[test]
190 fn unequal_close_yields_zero() {
191 let mut t = Counterattack::new();
192 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
193 assert_eq!(t.update(c(10.0, 17.1, 9.9, 17.0, 1)), Some(0.0));
195 }
196
197 #[test]
198 fn same_color_yields_zero() {
199 let mut t = Counterattack::new();
200 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
202 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 1)), Some(0.0));
203 }
204
205 #[test]
206 fn short_body_yields_zero() {
207 let mut t = Counterattack::new();
208 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
210 assert_eq!(t.update(c(14.8, 20.0, 9.9, 15.2, 1)), Some(0.0));
211 }
212
213 #[test]
214 fn first_bar_returns_zero() {
215 let mut t = Counterattack::new();
216 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
217 }
218
219 #[test]
220 fn batch_equals_streaming() {
221 let candles: Vec<Candle> = (0..40)
222 .map(|i| {
223 let base = 100.0 + i as f64;
224 c(base, base + 2.0, base - 2.0, base + 1.5, i)
225 })
226 .collect();
227 let mut a = Counterattack::new();
228 let mut b = Counterattack::new();
229 assert_eq!(
230 a.batch(&candles),
231 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
232 );
233 }
234
235 #[test]
236 fn reset_clears_state() {
237 let mut t = Counterattack::new();
238 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
239 t.update(c(10.0, 15.1, 9.9, 15.0, 1));
240 assert!(t.is_ready());
241 t.reset();
242 assert!(!t.is_ready());
243 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
244 }
245}