mantis_ta/indicators/trend/
ichimoku.rs1use crate::indicators::Indicator;
2use crate::types::{Candle, IchimokuOutput};
3use crate::utils::ringbuf::RingBuf;
4
5#[derive(Debug, Clone)]
46pub struct Ichimoku {
47 tenkan_period: usize,
48 kijun_period: usize,
49 senkou_b_period: usize,
50 tenkan_window: RingBuf<(f64, f64)>,
51 kijun_window: RingBuf<(f64, f64)>,
52 senkou_b_window: RingBuf<(f64, f64)>,
53}
54
55impl Ichimoku {
56 pub fn new(tenkan_period: usize, kijun_period: usize, senkou_b_period: usize) -> Self {
57 assert!(
58 tenkan_period > 0 && kijun_period > 0 && senkou_b_period > 0,
59 "periods must be > 0"
60 );
61 Self {
62 tenkan_period,
63 kijun_period,
64 senkou_b_period,
65 tenkan_window: RingBuf::new(tenkan_period, (0.0, 0.0)),
66 kijun_window: RingBuf::new(kijun_period, (0.0, 0.0)),
67 senkou_b_window: RingBuf::new(senkou_b_period, (0.0, 0.0)),
68 }
69 }
70
71 #[inline]
72 fn midpoint(window: &RingBuf<(f64, f64)>, period: usize) -> Option<f64> {
73 if window.len() < period {
74 return None;
75 }
76 let mut highest = f64::MIN;
77 let mut lowest = f64::MAX;
78 for &(high, low) in window.iter() {
79 if high > highest {
80 highest = high;
81 }
82 if low < lowest {
83 lowest = low;
84 }
85 }
86 Some((highest + lowest) / 2.0)
87 }
88}
89
90impl Indicator for Ichimoku {
91 type Output = IchimokuOutput;
92
93 fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
94 self.tenkan_window.push((candle.high, candle.low));
95 self.kijun_window.push((candle.high, candle.low));
96 self.senkou_b_window.push((candle.high, candle.low));
97
98 let tenkan_sen = Self::midpoint(&self.tenkan_window, self.tenkan_period)?;
99 let kijun_sen = Self::midpoint(&self.kijun_window, self.kijun_period)?;
100 let senkou_span_b = Self::midpoint(&self.senkou_b_window, self.senkou_b_period)?;
101 let senkou_span_a = (tenkan_sen + kijun_sen) / 2.0;
102
103 Some(IchimokuOutput {
104 tenkan_sen,
105 kijun_sen,
106 senkou_span_a,
107 senkou_span_b,
108 chikou_span: candle.close,
109 })
110 }
111
112 fn reset(&mut self) {
113 self.tenkan_window = RingBuf::new(self.tenkan_period, (0.0, 0.0));
114 self.kijun_window = RingBuf::new(self.kijun_period, (0.0, 0.0));
115 self.senkou_b_window = RingBuf::new(self.senkou_b_period, (0.0, 0.0));
116 }
117
118 fn warmup_period(&self) -> usize {
119 self.tenkan_period
120 .max(self.kijun_period)
121 .max(self.senkou_b_period)
122 }
123
124 fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
125 Box::new(self.clone())
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn make_candles(n: usize) -> Vec<Candle> {
134 (0..n)
135 .map(|i| {
136 let price = 100.0 + i as f64;
137 Candle {
138 timestamp: i as i64,
139 open: price,
140 high: price + 1.0,
141 low: price - 1.0,
142 close: price,
143 volume: 0.0,
144 }
145 })
146 .collect()
147 }
148
149 #[test]
150 fn ichimoku_emits_after_warmup() {
151 let candles = make_candles(60);
152 let ichimoku = Ichimoku::new(9, 26, 52);
153 let out = ichimoku.calculate(&candles);
154
155 assert!(out.iter().take(51).all(|v| v.is_none()));
156 assert!(out[51].is_some());
157 }
158
159 #[test]
160 fn ichimoku_warmup_period_is_max_of_periods() {
161 assert_eq!(Ichimoku::new(9, 26, 52).warmup_period(), 52);
162 assert_eq!(Ichimoku::new(9, 52, 26).warmup_period(), 52);
163 }
164
165 #[test]
166 fn ichimoku_reset_restores_fresh_state() {
167 let candles = make_candles(60);
168 let mut ichimoku = Ichimoku::new(9, 26, 52);
169 for candle in &candles {
170 ichimoku.next(candle);
171 }
172 assert!(ichimoku.next(&candles[0]).is_some());
173
174 ichimoku.reset();
175 assert_eq!(ichimoku.next(&candles[0]), None);
176 }
177
178 #[test]
179 #[should_panic(expected = "periods must be > 0")]
180 fn ichimoku_rejects_zero_period() {
181 Ichimoku::new(0, 26, 52);
182 }
183}