wickra_core/indicators/
kagi_bars.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::BarBuilder;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct KagiBar {
10 pub start: f64,
12 pub end: f64,
14 pub direction: i8,
16}
17
18#[derive(Debug, Clone)]
48pub struct KagiBars {
49 reversal: f64,
50 dir: i8,
51 extreme: Option<f64>,
52 segment_start: f64,
53}
54
55impl KagiBars {
56 pub fn new(reversal: f64) -> Result<Self> {
62 if !reversal.is_finite() || reversal <= 0.0 {
63 return Err(Error::InvalidPeriod {
64 message: "reversal must be finite and positive",
65 });
66 }
67 Ok(Self {
68 reversal,
69 dir: 0,
70 extreme: None,
71 segment_start: 0.0,
72 })
73 }
74
75 pub const fn reversal(&self) -> f64 {
77 self.reversal
78 }
79
80 pub const fn extreme(&self) -> Option<f64> {
82 self.extreme
83 }
84}
85
86impl BarBuilder for KagiBars {
87 type Bar = KagiBar;
88
89 fn update(&mut self, candle: Candle) -> Vec<KagiBar> {
90 let close = candle.close;
91 let Some(mut ext) = self.extreme else {
92 self.extreme = Some(close);
93 self.segment_start = close;
94 return Vec::new();
95 };
96 let mut bars = Vec::new();
97 match self.dir {
98 0 => {
99 if close > ext {
100 self.dir = 1;
101 ext = close;
102 } else if close < ext {
103 self.dir = -1;
104 ext = close;
105 }
106 }
107 1 => {
108 if close > ext {
109 ext = close;
110 } else if close <= ext - self.reversal {
111 bars.push(KagiBar {
112 start: self.segment_start,
113 end: ext,
114 direction: 1,
115 });
116 self.segment_start = ext;
117 self.dir = -1;
118 ext = close;
119 }
120 }
121 _ => {
122 if close < ext {
123 ext = close;
124 } else if close >= ext + self.reversal {
125 bars.push(KagiBar {
126 start: self.segment_start,
127 end: ext,
128 direction: -1,
129 });
130 self.segment_start = ext;
131 self.dir = 1;
132 ext = close;
133 }
134 }
135 }
136 self.extreme = Some(ext);
137 bars
138 }
139
140 fn reset(&mut self) {
141 self.dir = 0;
142 self.extreme = None;
143 self.segment_start = 0.0;
144 }
145
146 #[inline]
147 fn name(&self) -> &'static str {
148 "KagiBars"
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use approx::assert_relative_eq;
156
157 fn flat(price: f64) -> Candle {
158 Candle::new(price, price, price, price, 1.0, 0).unwrap()
159 }
160
161 #[test]
162 fn rejects_invalid_reversal() {
163 assert!(matches!(
164 KagiBars::new(0.0),
165 Err(Error::InvalidPeriod { .. })
166 ));
167 assert!(matches!(
168 KagiBars::new(-2.0),
169 Err(Error::InvalidPeriod { .. })
170 ));
171 assert!(matches!(
172 KagiBars::new(f64::INFINITY),
173 Err(Error::InvalidPeriod { .. })
174 ));
175 }
176
177 #[test]
178 fn accessors_and_metadata() {
179 let kagi = KagiBars::new(2.0).unwrap();
180 assert_eq!(kagi.name(), "KagiBars");
181 assert_relative_eq!(kagi.reversal(), 2.0, epsilon = 1e-12);
182 assert_eq!(kagi.extreme(), None);
183 }
184
185 #[test]
186 fn seeds_then_establishes_up_direction() {
187 let mut kagi = KagiBars::new(2.0).unwrap();
188 assert!(kagi.update(flat(10.0)).is_empty()); assert_eq!(kagi.extreme(), Some(10.0));
190 assert!(kagi.update(flat(11.0)).is_empty()); assert_eq!(kagi.extreme(), Some(11.0));
192 }
193
194 #[test]
195 fn establishes_down_direction_from_seed() {
196 let mut kagi = KagiBars::new(2.0).unwrap();
197 kagi.update(flat(10.0));
198 assert!(kagi.update(flat(9.0)).is_empty()); assert_eq!(kagi.extreme(), Some(9.0));
200 }
201
202 #[test]
203 fn extends_without_emitting() {
204 let mut kagi = KagiBars::new(2.0).unwrap();
205 kagi.update(flat(10.0));
206 kagi.update(flat(11.0));
207 assert!(kagi.update(flat(15.0)).is_empty()); assert_eq!(kagi.extreme(), Some(15.0));
209 }
210
211 #[test]
212 fn reversal_closes_up_segment() {
213 let mut kagi = KagiBars::new(2.0).unwrap();
214 kagi.update(flat(10.0));
215 kagi.update(flat(11.0));
216 kagi.update(flat(15.0));
217 let bars = kagi.update(flat(12.0)); assert_eq!(bars.len(), 1);
219 assert_eq!(bars[0].direction, 1);
220 assert_relative_eq!(bars[0].start, 10.0, epsilon = 1e-12);
221 assert_relative_eq!(bars[0].end, 15.0, epsilon = 1e-12);
222 assert_eq!(kagi.extreme(), Some(12.0));
223 }
224
225 #[test]
226 fn reversal_closes_down_segment() {
227 let mut kagi = KagiBars::new(2.0).unwrap();
228 kagi.update(flat(10.0));
229 kagi.update(flat(11.0));
230 kagi.update(flat(15.0));
231 kagi.update(flat(12.0)); let bars = kagi.update(flat(20.0)); assert_eq!(bars.len(), 1);
234 assert_eq!(bars[0].direction, -1);
235 assert_relative_eq!(bars[0].start, 15.0, epsilon = 1e-12);
236 assert_relative_eq!(bars[0].end, 12.0, epsilon = 1e-12);
237 }
238
239 #[test]
240 fn small_pullback_does_not_reverse() {
241 let mut kagi = KagiBars::new(2.0).unwrap();
242 kagi.update(flat(10.0));
243 kagi.update(flat(11.0));
244 kagi.update(flat(15.0));
245 assert!(kagi.update(flat(14.0)).is_empty()); assert_eq!(kagi.extreme(), Some(15.0));
247 }
248
249 #[test]
250 fn down_trend_small_bounce_does_not_reverse() {
251 let mut kagi = KagiBars::new(2.0).unwrap();
252 kagi.update(flat(10.0));
253 kagi.update(flat(9.0)); kagi.update(flat(5.0)); assert!(kagi.update(flat(6.0)).is_empty()); assert_eq!(kagi.extreme(), Some(5.0));
257 }
258
259 #[test]
260 fn reset_clears_state() {
261 let mut kagi = KagiBars::new(2.0).unwrap();
262 kagi.update(flat(10.0));
263 kagi.update(flat(15.0));
264 kagi.reset();
265 assert_eq!(kagi.extreme(), None);
266 assert!(kagi.update(flat(99.0)).is_empty());
267 assert_eq!(kagi.extreme(), Some(99.0));
268 }
269
270 #[test]
271 fn batch_collects_completed_segments() {
272 let mut kagi = KagiBars::new(2.0).unwrap();
273 let candles = [
274 flat(10.0),
275 flat(15.0),
276 flat(12.0), flat(20.0), ];
279 let bars = kagi.batch(&candles);
280 assert_eq!(bars.len(), 2);
281 assert_eq!(bars[0].direction, 1);
282 assert_eq!(bars[1].direction, -1);
283 }
284}