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 fn update(&mut self, candle: Candle) -> Option<f64> {
68 self.has_emitted = true;
69 let prev = self.prev;
70 self.prev = Some(candle);
71 let Some(bar1) = prev else {
72 return Some(0.0);
73 };
74 if !is_marubozu(&bar1) || !is_marubozu(&candle) {
75 return Some(0.0);
76 }
77 if bar1.close < bar1.open && candle.close > candle.open && candle.low > bar1.high {
79 return Some(1.0);
80 }
81 if bar1.close > bar1.open && candle.close < candle.open && candle.high < bar1.low {
83 return Some(-1.0);
84 }
85 Some(0.0)
86 }
87
88 fn reset(&mut self) {
89 self.prev = None;
90 self.has_emitted = false;
91 }
92
93 fn warmup_period(&self) -> usize {
94 2
95 }
96
97 fn is_ready(&self) -> bool {
98 self.has_emitted
99 }
100
101 fn name(&self) -> &'static str {
102 "Kicking"
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::traits::BatchExt;
110
111 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
112 Candle::new(open, high, low, close, 1.0, ts).unwrap()
113 }
114
115 #[test]
116 fn accessors_and_metadata() {
117 let t = Kicking::new();
118 assert_eq!(t.name(), "Kicking");
119 assert_eq!(t.warmup_period(), 2);
120 assert!(!t.is_ready());
121 }
122
123 #[test]
124 fn bullish_kicking_is_plus_one() {
125 let mut t = Kicking::new();
126 assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
127 assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(1.0));
128 }
129
130 #[test]
131 fn bearish_kicking_is_minus_one() {
132 let mut t = Kicking::new();
133 assert_eq!(t.update(c(10.0, 12.0, 10.0, 12.0, 0)), Some(0.0));
134 assert_eq!(t.update(c(8.0, 8.0, 6.0, 6.0, 1)), Some(-1.0));
135 }
136
137 #[test]
138 fn not_marubozu_yields_zero() {
139 let mut t = Kicking::new();
140 t.update(c(12.0, 14.0, 8.0, 10.0, 0));
142 assert_eq!(t.update(c(14.0, 16.0, 14.0, 16.0, 1)), Some(0.0));
143 }
144
145 #[test]
146 fn no_gap_yields_zero() {
147 let mut t = Kicking::new();
148 t.update(c(12.0, 12.0, 10.0, 10.0, 0));
149 assert_eq!(t.update(c(11.0, 13.0, 11.0, 13.0, 1)), Some(0.0));
151 }
152
153 #[test]
154 fn first_bar_returns_zero() {
155 let mut t = Kicking::new();
156 assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
157 }
158
159 #[test]
160 fn batch_equals_streaming() {
161 let candles: Vec<Candle> = (0..40)
162 .map(|i| {
163 let base = 100.0 + i as f64 * 5.0;
164 if i % 2 == 0 {
165 c(base + 2.0, base + 2.0, base, base, i)
166 } else {
167 c(base + 3.0, base + 5.0, base + 3.0, base + 5.0, i)
168 }
169 })
170 .collect();
171 let mut a = Kicking::new();
172 let mut b = Kicking::new();
173 assert_eq!(
174 a.batch(&candles),
175 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
176 );
177 }
178
179 #[test]
180 fn reset_clears_state() {
181 let mut t = Kicking::new();
182 t.update(c(12.0, 12.0, 10.0, 10.0, 0));
183 t.update(c(14.0, 16.0, 14.0, 16.0, 1));
184 assert!(t.is_ready());
185 t.reset();
186 assert!(!t.is_ready());
187 assert_eq!(t.update(c(12.0, 12.0, 10.0, 10.0, 0)), Some(0.0));
188 }
189}