1#![allow(clippy::too_many_arguments)]
24
25use std::collections::VecDeque;
26
27use crate::error::{Error, Result};
28use crate::ohlcv::Candle;
29use crate::traits::Indicator;
30
31#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct IchimokuOutput {
41 pub tenkan: Option<f64>,
43 pub kijun: Option<f64>,
45 pub senkou_a: Option<f64>,
48 pub senkou_b: Option<f64>,
51 pub chikou: Option<f64>,
53}
54
55#[derive(Debug, Clone)]
79pub struct Ichimoku {
80 tenkan_period: usize,
81 kijun_period: usize,
82 senkou_b_period: usize,
83 displacement: usize,
84 highs: VecDeque<f64>,
86 lows: VecDeque<f64>,
87 senkou_a_history: VecDeque<f64>,
89 senkou_b_history: VecDeque<f64>,
91 close_history: VecDeque<f64>,
93 last: Option<IchimokuOutput>,
94}
95
96impl Ichimoku {
97 pub fn new(
110 tenkan_period: usize,
111 kijun_period: usize,
112 senkou_b_period: usize,
113 displacement: usize,
114 ) -> Result<Self> {
115 if tenkan_period == 0 || kijun_period == 0 || senkou_b_period == 0 || displacement == 0 {
116 return Err(Error::PeriodZero);
117 }
118 if tenkan_period >= kijun_period || kijun_period >= senkou_b_period {
119 return Err(Error::InvalidPeriod {
120 message: "Ichimoku periods must satisfy tenkan < kijun < senkou_b",
121 });
122 }
123 let cap = senkou_b_period;
124 Ok(Self {
125 tenkan_period,
126 kijun_period,
127 senkou_b_period,
128 displacement,
129 highs: VecDeque::with_capacity(cap),
130 lows: VecDeque::with_capacity(cap),
131 senkou_a_history: VecDeque::with_capacity(displacement),
132 senkou_b_history: VecDeque::with_capacity(displacement),
133 close_history: VecDeque::with_capacity(displacement),
134 last: None,
135 })
136 }
137
138 pub fn classic() -> Self {
140 Self::new(9, 26, 52, 26).expect("classic Ichimoku periods are valid")
141 }
142
143 pub const fn periods(&self) -> (usize, usize, usize, usize) {
145 (
146 self.tenkan_period,
147 self.kijun_period,
148 self.senkou_b_period,
149 self.displacement,
150 )
151 }
152
153 pub const fn value(&self) -> Option<IchimokuOutput> {
155 self.last
156 }
157
158 fn midpoint(&self, n: usize) -> f64 {
161 let len = self.highs.len();
162 let start = len - n;
163 let mut hi = f64::NEG_INFINITY;
164 let mut lo = f64::INFINITY;
165 for i in start..len {
166 hi = hi.max(self.highs[i]);
167 lo = lo.min(self.lows[i]);
168 }
169 f64::midpoint(hi, lo)
170 }
171}
172
173impl Indicator for Ichimoku {
174 type Input = Candle;
175 type Output = IchimokuOutput;
176
177 fn update(&mut self, candle: Candle) -> Option<IchimokuOutput> {
178 if self.highs.len() == self.senkou_b_period {
180 self.highs.pop_front();
181 self.lows.pop_front();
182 }
183 self.highs.push_back(candle.high);
184 self.lows.push_back(candle.low);
185
186 let tenkan =
187 (self.highs.len() >= self.tenkan_period).then(|| self.midpoint(self.tenkan_period));
188 let kijun =
189 (self.highs.len() >= self.kijun_period).then(|| self.midpoint(self.kijun_period));
190 let senkou_b_now =
191 (self.highs.len() >= self.senkou_b_period).then(|| self.midpoint(self.senkou_b_period));
192
193 let senkou_a_now = match (tenkan, kijun) {
196 (Some(t), Some(k)) => Some(f64::midpoint(t, k)),
197 _ => None,
198 };
199
200 let push_or_nan = |q: &mut VecDeque<f64>, v: Option<f64>, cap: usize| {
205 if q.len() == cap {
206 q.pop_front();
207 }
208 q.push_back(v.unwrap_or(f64::NAN));
209 };
210 push_or_nan(&mut self.senkou_a_history, senkou_a_now, self.displacement);
211 push_or_nan(&mut self.senkou_b_history, senkou_b_now, self.displacement);
212
213 let take_front = |q: &VecDeque<f64>, cap: usize| -> Option<f64> {
217 if q.len() == cap {
218 let v = q[0];
219 if v.is_nan() {
220 None
221 } else {
222 Some(v)
223 }
224 } else {
225 None
226 }
227 };
228 let senkou_a = take_front(&self.senkou_a_history, self.displacement);
229 let senkou_b = take_front(&self.senkou_b_history, self.displacement);
230
231 if self.close_history.len() == self.displacement {
233 self.close_history.pop_front();
234 }
235 self.close_history.push_back(candle.close);
236 let chikou = (self.close_history.len() == self.displacement).then(|| self.close_history[0]);
237
238 let out = IchimokuOutput {
239 tenkan,
240 kijun,
241 senkou_a,
242 senkou_b,
243 chikou,
244 };
245 self.last = Some(out);
246 Some(out)
247 }
248
249 fn reset(&mut self) {
250 self.highs.clear();
251 self.lows.clear();
252 self.senkou_a_history.clear();
253 self.senkou_b_history.clear();
254 self.close_history.clear();
255 self.last = None;
256 }
257
258 fn warmup_period(&self) -> usize {
259 self.senkou_b_period + self.displacement - 1
262 }
263
264 fn is_ready(&self) -> bool {
265 self.last.is_some_and(|o| {
266 o.tenkan.is_some()
267 && o.kijun.is_some()
268 && o.senkou_a.is_some()
269 && o.senkou_b.is_some()
270 && o.chikou.is_some()
271 })
272 }
273
274 fn name(&self) -> &'static str {
275 "Ichimoku"
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::traits::BatchExt;
283 use approx::assert_relative_eq;
284
285 fn c(h: f64, l: f64, cl: f64, i: i64) -> Candle {
286 Candle::new(cl, h, l, cl, 0.0, i).unwrap()
287 }
288
289 fn ramp(n: i64) -> Vec<Candle> {
290 (0..n)
291 .map(|i| {
292 let p = 100.0 + f64::from(i32::try_from(i).unwrap());
293 c(p + 2.0, p - 2.0, p + 1.0, i)
294 })
295 .collect()
296 }
297
298 #[test]
299 fn rejects_zero_periods() {
300 assert!(matches!(
301 Ichimoku::new(0, 26, 52, 26),
302 Err(Error::PeriodZero)
303 ));
304 assert!(matches!(
305 Ichimoku::new(9, 0, 52, 26),
306 Err(Error::PeriodZero)
307 ));
308 assert!(matches!(
309 Ichimoku::new(9, 26, 0, 26),
310 Err(Error::PeriodZero)
311 ));
312 assert!(matches!(
313 Ichimoku::new(9, 26, 52, 0),
314 Err(Error::PeriodZero)
315 ));
316 }
317
318 #[test]
319 fn rejects_non_increasing_periods() {
320 assert!(matches!(
321 Ichimoku::new(26, 26, 52, 26),
322 Err(Error::InvalidPeriod { .. })
323 ));
324 assert!(matches!(
325 Ichimoku::new(9, 52, 52, 26),
326 Err(Error::InvalidPeriod { .. })
327 ));
328 assert!(matches!(
329 Ichimoku::new(52, 26, 9, 26),
330 Err(Error::InvalidPeriod { .. })
331 ));
332 }
333
334 #[test]
335 fn accessors_and_metadata() {
336 let ichi = Ichimoku::classic();
337 assert_eq!(ichi.periods(), (9, 26, 52, 26));
338 assert_eq!(ichi.warmup_period(), 77);
339 assert_eq!(ichi.name(), "Ichimoku");
340 assert!(ichi.value().is_none());
341 }
342
343 #[test]
344 fn tenkan_emits_at_period() {
345 let mut ichi = Ichimoku::classic();
346 let candles = ramp(10);
347 let out = ichi.batch(&candles);
348 for (i, o) in out.iter().enumerate() {
350 let v = o.unwrap();
351 if i < 8 {
352 assert!(v.tenkan.is_none(), "tenkan must be None until 9 bars");
353 } else {
354 assert!(v.tenkan.is_some(), "tenkan must be Some from bar 9 on");
355 }
356 }
357 }
358
359 #[test]
360 fn fully_populated_after_warmup() {
361 let mut ichi = Ichimoku::classic();
362 let candles = ramp(120);
363 let out = ichi.batch(&candles);
364 let last = out.last().unwrap().unwrap();
365 assert!(last.tenkan.is_some());
366 assert!(last.kijun.is_some());
367 assert!(last.senkou_a.is_some());
368 assert!(last.senkou_b.is_some());
369 assert!(last.chikou.is_some());
370 assert!(ichi.is_ready());
371 }
372
373 #[test]
374 fn ramp_tenkan_equals_window_midpoint() {
375 let mut ichi = Ichimoku::classic();
378 let candles = ramp(20);
379 let out = ichi.batch(&candles);
380 let v = out[8].unwrap();
383 assert_relative_eq!(v.tenkan.unwrap(), 104.0, epsilon = 1e-12);
384 }
385
386 #[test]
387 fn chikou_is_close_displacement_bars_back() {
388 let mut ichi = Ichimoku::classic();
389 let candles = ramp(60);
390 let out = ichi.batch(&candles);
391 let v = out[25].unwrap();
393 assert_relative_eq!(v.chikou.unwrap(), candles[0].close, epsilon = 1e-12);
394 let v = out[50].unwrap();
395 assert_relative_eq!(v.chikou.unwrap(), candles[25].close, epsilon = 1e-12);
396 }
397
398 #[test]
399 fn batch_equals_streaming() {
400 let candles = ramp(120);
401 let mut a = Ichimoku::classic();
402 let mut b = Ichimoku::classic();
403 let batched = a.batch(&candles);
404 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
405 assert_eq!(batched.len(), streamed.len());
406 for (lhs, rhs) in batched.iter().zip(streamed.iter()) {
407 let (l, r) = (lhs.unwrap(), rhs.unwrap());
408 assert_eq!(l.tenkan, r.tenkan);
409 assert_eq!(l.kijun, r.kijun);
410 assert_eq!(l.senkou_a, r.senkou_a);
411 assert_eq!(l.senkou_b, r.senkou_b);
412 assert_eq!(l.chikou, r.chikou);
413 }
414 }
415
416 #[test]
417 fn reset_clears_state() {
418 let mut ichi = Ichimoku::classic();
419 ichi.batch(&ramp(100));
420 assert!(ichi.is_ready());
421 ichi.reset();
422 assert!(!ichi.is_ready());
423 assert!(ichi.value().is_none());
424 }
425
426 #[test]
427 fn custom_periods_accepted() {
428 let mut ichi = Ichimoku::new(5, 10, 20, 10).unwrap();
429 let out = ichi.batch(&ramp(40));
430 let last = out.last().unwrap().unwrap();
431 assert!(last.tenkan.is_some());
432 assert!(last.senkou_a.is_some());
433 }
434}