wickra_core/indicators/
kicking.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
42pub struct Kicking {
43 prev: Option<Candle>,
44 has_emitted: bool,
45}
46
47impl Kicking {
48 pub const fn new() -> Self {
50 Self {
51 prev: None,
52 has_emitted: false,
53 }
54 }
55}
56
57fn is_marubozu(candle: &Candle) -> bool {
59 let range = candle.high - candle.low;
60 range > 0.0 && (candle.close - candle.open).abs() >= 0.95 * range
61}
62
63impl Indicator for Kicking {
64 type Input = Candle;
65 type Output = f64;
66
67 #[inline]
68 fn update(&mut self, candle: Candle) -> Option<f64> {
69 let prev = self.prev;
70 self.prev = Some(candle);
71 let bar1 = prev?;
72 self.has_emitted = true;
73 if !is_marubozu(&bar1) || !is_marubozu(&candle) {
74 return Some(0.0);
75 }
76 if bar1.close < bar1.open && candle.close > candle.open && candle.low > bar1.high {
78 return Some(1.0);
79 }
80 if bar1.close > bar1.open && candle.close < candle.open && candle.high < bar1.low {
82 return Some(-1.0);
83 }
84 Some(0.0)
85 }
86
87 fn reset(&mut self) {
88 self.prev = None;
89 self.has_emitted = false;
90 }
91
92 #[inline]
93 fn warmup_period(&self) -> usize {
94 2
95 }
96
97 #[inline]
98 fn is_ready(&self) -> bool {
99 self.has_emitted
100 }
101
102 #[inline]
103 fn name(&self) -> &'static str {
104 "Kicking"
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::traits::BatchExt;
112
113 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
114 Candle::new(open, high, low, close, 1.0, ts).unwrap()
115 }
116
117 #[test]
118 fn accessors_and_metadata() {
119 let t = Kicking::new();
120 assert_eq!(t.name(), "Kicking");
121 assert_eq!(t.warmup_period(), 2);
122 assert!(!t.is_ready());
123 }
124
125 #[test]
126 fn bullish_kicking_is_plus_one() {
127 let mut t = Kicking::new();
128 assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), None);
129 assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(1.0));
130 }
131
132 #[test]
133 fn bearish_kicking_is_minus_one() {
134 let mut t = Kicking::new();
135 assert_eq!(t.update(c(10.0, 12.0, 10.0, 12.0, 0)), None);
136 assert_eq!(t.update(c(8.0, 8.0, 6.0, 6.0, 1)), Some(-1.0));
137 }
138
139 #[test]
140 fn not_marubozu_yields_zero() {
141 let mut t = Kicking::new();
142 t.update(c(12.0, 14.0, 8.0, 10.0, 0));
144 assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(0.0));
145 }
146
147 #[test]
148 fn no_gap_yields_zero() {
149 let mut t = Kicking::new();
150 t.update(c(12.0, 12.0, 10.0, 10.0, 0));
151 assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
153 }
154
155 #[test]
156 fn first_bar_returns_zero() {
157 let mut t = Kicking::new();
158 assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), None);
159 }
160
161 #[test]
162 fn batch_equals_streaming() {
163 let candles: Vec<Candle> = (0..40)
164 .map(|i| {
165 let base = 100.0 + i as f64 * 5.0;
166 if i % 2 == 0 {
167 c(base + 2.0, base + 2.0, base, base, i)
168 } else {
169 c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
170 }
171 })
172 .collect();
173 let mut a = Kicking::new();
174 let mut b = Kicking::new();
175 assert_eq!(
176 a.batch(&candles),
177 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
178 );
179 }
180
181 #[test]
182 fn reset_clears_state() {
183 let mut t = Kicking::new();
184 t.update(c(12.0, 12.0, 10.0, 10.0, 0));
185 t.update(c(14.0, 16.0, 14.0, 16.0, 1));
186 assert!(t.is_ready());
187 t.reset();
188 assert!(!t.is_ready());
189 assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), None);
190 }
191}