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 fn name(&self) -> &'static str {
147 "KagiBars"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use approx::assert_relative_eq;
155
156 fn flat(price: f64) -> Candle {
157 Candle::new(price, price, price, price, 1.0, 0).unwrap()
158 }
159
160 #[test]
161 fn rejects_invalid_reversal() {
162 assert!(matches!(
163 KagiBars::new(0.0),
164 Err(Error::InvalidPeriod { .. })
165 ));
166 assert!(matches!(
167 KagiBars::new(-2.0),
168 Err(Error::InvalidPeriod { .. })
169 ));
170 assert!(matches!(
171 KagiBars::new(f64::INFINITY),
172 Err(Error::InvalidPeriod { .. })
173 ));
174 }
175
176 #[test]
177 fn accessors_and_metadata() {
178 let kagi = KagiBars::new(2.0).unwrap();
179 assert_eq!(kagi.name(), "KagiBars");
180 assert_relative_eq!(kagi.reversal(), 2.0, epsilon = 1e-12);
181 assert_eq!(kagi.extreme(), None);
182 }
183
184 #[test]
185 fn seeds_then_establishes_up_direction() {
186 let mut kagi = KagiBars::new(2.0).unwrap();
187 assert!(kagi.update(flat(10.0)).is_empty()); assert_eq!(kagi.extreme(), Some(10.0));
189 assert!(kagi.update(flat(11.0)).is_empty()); assert_eq!(kagi.extreme(), Some(11.0));
191 }
192
193 #[test]
194 fn establishes_down_direction_from_seed() {
195 let mut kagi = KagiBars::new(2.0).unwrap();
196 kagi.update(flat(10.0));
197 assert!(kagi.update(flat(9.0)).is_empty()); assert_eq!(kagi.extreme(), Some(9.0));
199 }
200
201 #[test]
202 fn extends_without_emitting() {
203 let mut kagi = KagiBars::new(2.0).unwrap();
204 kagi.update(flat(10.0));
205 kagi.update(flat(11.0));
206 assert!(kagi.update(flat(15.0)).is_empty()); assert_eq!(kagi.extreme(), Some(15.0));
208 }
209
210 #[test]
211 fn reversal_closes_up_segment() {
212 let mut kagi = KagiBars::new(2.0).unwrap();
213 kagi.update(flat(10.0));
214 kagi.update(flat(11.0));
215 kagi.update(flat(15.0));
216 let bars = kagi.update(flat(12.0)); assert_eq!(bars.len(), 1);
218 assert_eq!(bars[0].direction, 1);
219 assert_relative_eq!(bars[0].start, 10.0, epsilon = 1e-12);
220 assert_relative_eq!(bars[0].end, 15.0, epsilon = 1e-12);
221 assert_eq!(kagi.extreme(), Some(12.0));
222 }
223
224 #[test]
225 fn reversal_closes_down_segment() {
226 let mut kagi = KagiBars::new(2.0).unwrap();
227 kagi.update(flat(10.0));
228 kagi.update(flat(11.0));
229 kagi.update(flat(15.0));
230 kagi.update(flat(12.0)); let bars = kagi.update(flat(20.0)); assert_eq!(bars.len(), 1);
233 assert_eq!(bars[0].direction, -1);
234 assert_relative_eq!(bars[0].start, 15.0, epsilon = 1e-12);
235 assert_relative_eq!(bars[0].end, 12.0, epsilon = 1e-12);
236 }
237
238 #[test]
239 fn small_pullback_does_not_reverse() {
240 let mut kagi = KagiBars::new(2.0).unwrap();
241 kagi.update(flat(10.0));
242 kagi.update(flat(11.0));
243 kagi.update(flat(15.0));
244 assert!(kagi.update(flat(14.0)).is_empty()); assert_eq!(kagi.extreme(), Some(15.0));
246 }
247
248 #[test]
249 fn down_trend_small_bounce_does_not_reverse() {
250 let mut kagi = KagiBars::new(2.0).unwrap();
251 kagi.update(flat(10.0));
252 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));
256 }
257
258 #[test]
259 fn reset_clears_state() {
260 let mut kagi = KagiBars::new(2.0).unwrap();
261 kagi.update(flat(10.0));
262 kagi.update(flat(15.0));
263 kagi.reset();
264 assert_eq!(kagi.extreme(), None);
265 assert!(kagi.update(flat(99.0)).is_empty());
266 assert_eq!(kagi.extreme(), Some(99.0));
267 }
268
269 #[test]
270 fn batch_collects_completed_segments() {
271 let mut kagi = KagiBars::new(2.0).unwrap();
272 let candles = [
273 flat(10.0),
274 flat(15.0),
275 flat(12.0), flat(20.0), ];
278 let bars = kagi.batch(&candles);
279 assert_eq!(bars.len(), 2);
280 assert_eq!(bars[0].direction, 1);
281 assert_eq!(bars[1].direction, -1);
282 }
283}