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 #[inline]
99 fn update(&mut self, candle: Candle) -> Option<f64> {
100 let prev = self.prev;
101 self.prev = Some(candle);
102 let bar1 = prev?;
103 self.has_emitted = true;
104 let range1 = bar1.high - bar1.low;
105 let range2 = candle.high - candle.low;
106 let body1 = bar1.close - bar1.open;
107 let body2 = candle.close - candle.open;
108 let long1 = body1.abs() >= 0.5 * range1;
109 let long2 = body2.abs() >= 0.5 * range2;
110 let tol = self.equal_tolerance * 0.5 * (range1 + range2);
111 let equal_close = (candle.close - bar1.close).abs() <= tol;
112 if !(long1 && long2 && equal_close) {
113 return Some(0.0);
114 }
115 if body1 < 0.0 && body2 > 0.0 {
117 return Some(1.0);
118 }
119 if body1 > 0.0 && body2 < 0.0 {
121 return Some(-1.0);
122 }
123 Some(0.0)
124 }
125
126 fn reset(&mut self) {
127 self.prev = None;
128 self.has_emitted = false;
129 }
130
131 #[inline]
132 fn warmup_period(&self) -> usize {
133 2
134 }
135
136 #[inline]
137 fn is_ready(&self) -> bool {
138 self.has_emitted
139 }
140
141 #[inline]
142 fn name(&self) -> &'static str {
143 "Counterattack"
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::traits::BatchExt;
151
152 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
153 Candle::new(open, high, low, close, 1.0, ts).unwrap()
154 }
155
156 #[test]
157 fn rejects_invalid_tolerance() {
158 assert!(Counterattack::with_tolerance(-0.01).is_err());
159 assert!(Counterattack::with_tolerance(1.0).is_err());
160 }
161
162 #[test]
163 fn accepts_valid_tolerance() {
164 let t = Counterattack::with_tolerance(0.0).unwrap();
165 assert!((t.equal_tolerance() - 0.0).abs() < 1e-12);
166 }
167
168 #[test]
169 fn accessors_and_metadata() {
170 let t = Counterattack::default();
171 assert_eq!(t.name(), "Counterattack");
172 assert_eq!(t.warmup_period(), 2);
173 assert!(!t.is_ready());
174 assert!((t.equal_tolerance() - 0.05).abs() < 1e-12);
175 }
176
177 #[test]
178 fn bullish_counterattack_is_plus_one() {
179 let mut t = Counterattack::new();
180 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
181 assert_eq!(t.update(c(10.0, 15.1, 9.9, 15.0, 1)), Some(1.0));
182 }
183
184 #[test]
185 fn bearish_counterattack_is_minus_one() {
186 let mut t = Counterattack::new();
187 assert_eq!(t.update(c(15.0, 20.1, 14.9, 20.0, 0)), None);
188 assert_eq!(t.update(c(25.0, 25.1, 19.9, 20.0, 1)), Some(-1.0));
189 }
190
191 #[test]
192 fn unequal_close_yields_zero() {
193 let mut t = Counterattack::new();
194 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
195 assert_eq!(t.update(c(10.0, 17.1, 9.9, 17.0, 1)), Some(0.0));
197 }
198
199 #[test]
200 fn same_color_yields_zero() {
201 let mut t = Counterattack::new();
202 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
204 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 1)), Some(0.0));
205 }
206
207 #[test]
208 fn short_body_yields_zero() {
209 let mut t = Counterattack::new();
210 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
212 assert_eq!(t.update(c(14.8, 20.0, 9.9, 15.2, 1)), Some(0.0));
213 }
214
215 #[test]
216 fn first_bar_returns_zero() {
217 let mut t = Counterattack::new();
218 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
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 - 2.0, base + 1.5, i)
227 })
228 .collect();
229 let mut a = Counterattack::new();
230 let mut b = Counterattack::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 = Counterattack::new();
240 t.update(c(20.0, 20.1, 14.9, 15.0, 0));
241 t.update(c(10.0, 15.1, 9.9, 15.0, 1));
242 assert!(t.is_ready());
243 t.reset();
244 assert!(!t.is_ready());
245 assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
246 }
247}