1use super::PriceBasedSource;
21use crate::model::{Bar, PriceBuilder};
22use chrono::{DateTime, Utc};
23
24#[derive(Debug, Clone)]
26pub struct KagiLine {
27 pub ts: DateTime<Utc>,
29 pub start_price: f64,
31 pub end_price: f64,
33 pub thickness: KagiThickness,
35 pub crossing: Option<f64>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum KagiThickness {
44 Thin,
46 Thick,
48}
49
50impl KagiLine {
51 pub fn to_bar(&self) -> Bar {
53 Bar {
54 time: self.ts,
55 open: self.start_price,
56 high: self.start_price.max(self.end_price),
57 low: self.start_price.min(self.end_price),
58 close: self.end_price,
59 volume: 0.0, colors: None,
61 payload: None,
62 }
63 }
64
65 pub fn is_up(&self) -> bool {
67 self.end_price > self.start_price
68 }
69}
70
71#[derive(Debug, Clone)]
76pub struct KagiConfig {
77 pub reversal_amount: f64,
79 pub source: PriceBasedSource,
81}
82
83impl Default for KagiConfig {
84 fn default() -> Self {
85 Self {
86 reversal_amount: 1.0,
87 source: PriceBasedSource::Close,
88 }
89 }
90}
91
92impl KagiConfig {
93 pub fn new(reversal_amount: f64) -> Self {
95 Self {
96 reversal_amount,
97 source: PriceBasedSource::Close,
98 }
99 }
100
101 pub fn from_atr(bars: &[Bar], period: usize, multiplier: f64) -> Self {
103 Self {
104 reversal_amount: super::BoxSizeMethod::Atr { period, multiplier }
105 .resolve(0.0, bars)
106 .unwrap_or(f64::NAN),
107 source: PriceBasedSource::Close,
108 }
109 }
110
111 pub fn from_percentage(base_price: f64, percentage: f64) -> Self {
113 Self {
114 reversal_amount: base_price.abs() * (percentage / 100.0),
115 source: PriceBasedSource::Close,
116 }
117 }
118}
119
120pub fn to_kagi_lines(bars: &[Bar], config: &KagiConfig) -> Vec<KagiLine> {
127 let reversal = config.reversal_amount;
128 if bars.is_empty() || !(reversal > 0.0 && reversal.is_finite()) {
129 return Vec::new();
130 }
131
132 let mut builder = KagiBuilder::new(reversal, config.source);
133 for bar in bars {
134 builder.push(bar);
135 }
136 builder.elements()
137}
138
139pub(crate) const MAX_LINES: usize = 10_000;
140
141#[derive(Clone)]
143struct KagiState {
144 curr_price: f64,
145 line_start_price: f64,
146 line_direction_up: bool,
147 last_ts: DateTime<Utc>,
148 level: Option<f64>,
151 crossing: Option<f64>,
153 is_thick: bool,
154}
155
156impl KagiState {
157 fn line(&self) -> KagiLine {
158 KagiLine {
159 ts: self.last_ts,
160 start_price: self.line_start_price,
161 end_price: self.curr_price,
162 thickness: if self.is_thick {
163 KagiThickness::Thick
164 } else {
165 KagiThickness::Thin
166 },
167 crossing: self.crossing,
168 }
169 }
170
171 fn reverse(&mut self, price: f64) {
173 self.level = Some(self.line_start_price);
174 self.crossing = None;
175 self.line_start_price = self.curr_price;
176 self.curr_price = price;
177 self.line_direction_up = !self.line_direction_up;
178 }
179}
180
181#[derive(Clone)]
184pub(crate) struct KagiBuilder {
185 reversal: f64,
186 source: PriceBasedSource,
187 state: Option<KagiState>,
188 lines: Vec<KagiLine>,
189}
190
191impl KagiBuilder {
192 pub(crate) fn new(reversal: f64, source: PriceBasedSource) -> Self {
193 Self {
194 reversal,
195 source,
196 state: None,
197 lines: Vec::new(),
198 }
199 }
200}
201
202impl PriceBuilder for KagiBuilder {
203 type Element = KagiLine;
204
205 fn push(&mut self, bar: &Bar) {
206 let reversal = self.reversal;
207 let Some(st) = self.state.as_mut() else {
208 self.state = Some(KagiState {
209 curr_price: bar.close,
210 line_start_price: bar.close,
211 line_direction_up: true,
212 last_ts: bar.time,
213 level: None,
214 crossing: None,
215 is_thick: false,
216 });
217 return;
218 };
219 st.last_ts = bar.time;
220 let price = match self.source {
221 PriceBasedSource::Close => bar.close,
222 PriceBasedSource::HighLow if st.line_direction_up => {
223 if bar.high > st.curr_price {
224 bar.high
225 } else {
226 bar.low
227 }
228 }
229 PriceBasedSource::HighLow => {
230 if bar.low < st.curr_price {
231 bar.low
232 } else {
233 bar.high
234 }
235 }
236 };
237
238 let up = st.line_direction_up;
239 if up && price < st.curr_price - reversal || !up && price > st.curr_price + reversal {
240 self.lines.push(st.line());
241 st.reverse(price);
242 } else if up && price > st.curr_price || !up && price < st.curr_price {
243 st.curr_price = price;
244 } else {
245 return;
246 }
247
248 let up = st.line_direction_up;
251 if let Some(level) = st.level
252 && st.is_thick != up
253 && (up && st.curr_price > level || !up && st.curr_price < level)
254 {
255 st.is_thick = up;
256 st.crossing = Some(level);
257 }
258
259 if self.lines.len() == 2 * MAX_LINES {
261 self.lines.drain(..MAX_LINES);
262 }
263 }
264
265 fn elements(&self) -> Vec<KagiLine> {
266 let mut lines = self.lines.clone();
267 if let Some(st) = self
269 .state
270 .as_ref()
271 .filter(|st| st.line_start_price != st.curr_price)
272 {
273 lines.push(st.line());
274 }
275 lines.drain(..lines.len().saturating_sub(MAX_LINES));
276 lines
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use chrono::Duration;
284
285 #[test]
286 fn thickness_changes_where_a_line_crosses_the_prior_shoulder_or_waist() {
287 use KagiThickness::{Thick, Thin};
288 let start = Utc::now();
289 let bars: Vec<Bar> = [100.0, 105.0, 102.0, 107.0, 103.0, 101.0]
292 .into_iter()
293 .enumerate()
294 .map(|(i, close)| Bar {
295 time: start + Duration::minutes(i as i64),
296 open: close,
297 high: close,
298 low: close,
299 close,
300 volume: 0.0,
301 colors: None,
302 payload: None,
303 })
304 .collect();
305 let lines: Vec<_> = to_kagi_lines(&bars, &KagiConfig::new(1.0))
306 .into_iter()
307 .map(|l| (l.start_price, l.end_price, l.thickness, l.crossing))
308 .collect();
309 assert_eq!(
310 lines,
311 [
312 (100.0, 105.0, Thin, None),
313 (105.0, 102.0, Thin, None),
314 (102.0, 107.0, Thick, Some(105.0)),
315 (107.0, 101.0, Thin, Some(102.0)),
316 ]
317 );
318 }
319
320 #[test]
321 fn test_kagi_line_to_bar() {
322 let line = KagiLine {
323 ts: Utc::now(),
324 start_price: 100.0,
325 end_price: 105.0,
326 thickness: KagiThickness::Thick,
327 crossing: None,
328 };
329
330 let bar = line.to_bar();
331 assert_eq!(bar.open, 100.0);
332 assert_eq!(bar.close, 105.0);
333 assert_eq!(bar.high, 105.0);
334 assert_eq!(bar.low, 100.0);
335 }
336
337 #[test]
338 fn test_kagi_line_direction() {
339 let up_line = KagiLine {
340 ts: Utc::now(),
341 start_price: 100.0,
342 end_price: 105.0,
343 thickness: KagiThickness::Thick,
344 crossing: None,
345 };
346 assert!(up_line.is_up());
347
348 let down_line = KagiLine {
349 ts: Utc::now(),
350 start_price: 105.0,
351 end_price: 100.0,
352 thickness: KagiThickness::Thin,
353 crossing: None,
354 };
355 assert!(!down_line.is_up());
356 }
357}