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