use crate::model::Bar;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct KagiLine {
pub ts: DateTime<Utc>,
pub start_price: f64,
pub end_price: f64,
pub thickness: KagiThickness,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KagiThickness {
Thin,
Thick,
}
impl KagiLine {
pub fn to_bar(&self) -> Bar {
Bar {
time: self.ts,
open: self.start_price,
high: self.start_price.max(self.end_price),
low: self.start_price.min(self.end_price),
close: self.end_price,
volume: 0.0, }
}
pub fn is_up(&self) -> bool {
self.end_price > self.start_price
}
}
#[derive(Debug, Clone)]
pub struct KagiConfig {
pub reversal_amount: f64,
}
impl Default for KagiConfig {
fn default() -> Self {
Self {
reversal_amount: 1.0,
}
}
}
impl KagiConfig {
pub fn new(reversal_amount: f64) -> Self {
Self { reversal_amount }
}
pub fn from_atr(bars: &[Bar], period: usize, multiplier: f64) -> Self {
let atr = calculate_atr(bars, period);
Self {
reversal_amount: atr * multiplier,
}
}
pub fn from_percentage(base_price: f64, percentage: f64) -> Self {
Self {
reversal_amount: base_price * (percentage / 100.0),
}
}
}
pub fn to_kagi_lines(bars: &[Bar], config: &KagiConfig) -> Vec<KagiLine> {
if bars.is_empty() {
return Vec::new();
}
let (min_price, max_price) = bars.iter().fold((f64::MAX, f64::MIN), |(min, max), bar| {
(min.min(bar.low), max.max(bar.high))
});
let price_range = max_price - min_price;
let min_reversal = price_range / 500.0;
let reversal = config.reversal_amount.max(min_reversal).max(0.0001);
let mut lines = Vec::new();
const MAX_LINES: usize = 10_000;
let mut curr_price = bars[0].close;
let mut line_start_price = bars[0].close;
let mut line_direction_up = true;
let mut last_ts = bars[0].time;
let mut significant_high = curr_price;
let mut significant_low = curr_price;
let mut is_thick = curr_price > line_start_price;
for bar in bars.iter().skip(1) {
last_ts = bar.time;
let price = bar.close;
if line_direction_up {
if price > curr_price {
curr_price = price;
if price > significant_high {
significant_high = price;
}
} else if price < (curr_price - reversal) {
lines.push(KagiLine {
ts: last_ts,
start_price: line_start_price,
end_price: curr_price,
thickness: if is_thick {
KagiThickness::Thick
} else {
KagiThickness::Thin
},
});
if lines.len() >= MAX_LINES {
return lines;
}
line_start_price = curr_price;
curr_price = price;
line_direction_up = false;
if price < significant_low {
is_thick = false; significant_low = price;
}
}
} else {
if price < curr_price {
curr_price = price;
if price < significant_low {
significant_low = price;
}
} else if price > (curr_price + reversal) {
lines.push(KagiLine {
ts: last_ts,
start_price: line_start_price,
end_price: curr_price,
thickness: if is_thick {
KagiThickness::Thick
} else {
KagiThickness::Thin
},
});
if lines.len() >= MAX_LINES {
return lines;
}
line_start_price = curr_price;
curr_price = price;
line_direction_up = true;
if price > significant_high {
is_thick = true; significant_high = price;
}
}
}
}
if line_start_price != curr_price {
lines.push(KagiLine {
ts: last_ts,
start_price: line_start_price,
end_price: curr_price,
thickness: if is_thick {
KagiThickness::Thick
} else {
KagiThickness::Thin
},
});
}
lines
}
fn calculate_atr(bars: &[Bar], period: usize) -> f64 {
if bars.len() < period {
let sum: f64 = bars.iter().map(|b| b.high - b.low).sum();
return sum / bars.len() as f64;
}
let mut true_ranges = Vec::new();
for i in 1..bars.len() {
let high_low = bars[i].high - bars[i].low;
let high_close_prev = (bars[i].high - bars[i - 1].close).abs();
let low_close_prev = (bars[i].low - bars[i - 1].close).abs();
let true_range = high_low.max(high_close_prev).max(low_close_prev);
true_ranges.push(true_range);
}
let sum: f64 = true_ranges
.iter()
.skip(true_ranges.len().saturating_sub(period))
.sum();
sum / period.min(true_ranges.len()) as f64
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
fn create_test_bars() -> Vec<Bar> {
let start = Utc::now();
vec![
Bar {
time: start,
open: 100.0,
high: 102.0,
low: 99.0,
close: 100.0,
volume: 1000.0,
},
Bar {
time: start + Duration::minutes(1),
open: 100.0,
high: 105.0,
low: 99.0,
close: 105.0,
volume: 1000.0,
},
Bar {
time: start + Duration::minutes(2),
open: 105.0,
high: 106.0,
low: 101.0,
close: 102.0,
volume: 1000.0,
},
Bar {
time: start + Duration::minutes(3),
open: 102.0,
high: 108.0,
low: 101.0,
close: 107.0,
volume: 1000.0,
},
]
}
#[test]
fn test_kagi_line_creation() {
let bars = create_test_bars();
let config = KagiConfig::new(2.0);
let lines = to_kagi_lines(&bars, &config);
assert!(!lines.is_empty());
}
#[test]
fn test_kagi_line_to_bar() {
let line = KagiLine {
ts: Utc::now(),
start_price: 100.0,
end_price: 105.0,
thickness: KagiThickness::Thick,
};
let bar = line.to_bar();
assert_eq!(bar.open, 100.0);
assert_eq!(bar.close, 105.0);
assert_eq!(bar.high, 105.0);
assert_eq!(bar.low, 100.0);
}
#[test]
fn test_kagi_line_direction() {
let up_line = KagiLine {
ts: Utc::now(),
start_price: 100.0,
end_price: 105.0,
thickness: KagiThickness::Thick,
};
assert!(up_line.is_up());
let down_line = KagiLine {
ts: Utc::now(),
start_price: 105.0,
end_price: 100.0,
thickness: KagiThickness::Thin,
};
assert!(!down_line.is_up());
}
}