1use std::{collections::VecDeque, fmt::Display};
17
18use nautilus_core::correctness::FAILED;
19use nautilus_model::data::Bar;
20
21use crate::{
22 indicator::Indicator,
23 support::{MAX_PERIOD, is_valid_hlc},
24};
25
26#[repr(C)]
48#[derive(Debug)]
49#[cfg_attr(
50 feature = "python",
51 pyo3::pyclass(module = "nautilus_trader.indicators")
52)]
53#[cfg_attr(
54 feature = "python",
55 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
56)]
57pub struct IchimokuCloud {
58 pub tenkan_period: usize,
59 pub kijun_period: usize,
60 pub senkou_period: usize,
61 pub displacement: usize,
62 pub tenkan_sen: f64,
63 pub kijun_sen: f64,
64 pub senkou_span_a: f64,
65 pub senkou_span_b: f64,
66 pub chikou_span: f64,
67 pub has_tenkan: bool,
68 pub has_kijun: bool,
69 pub has_senkou_a: bool,
70 pub has_senkou_b: bool,
71 pub has_chikou: bool,
72 pub count: usize,
73 pub initialized: bool,
74 has_inputs: bool,
75 highs: VecDeque<f64>,
76 lows: VecDeque<f64>,
77 senkou_a_history: VecDeque<f64>,
78 senkou_b_history: VecDeque<f64>,
79 close_history: VecDeque<f64>,
80}
81
82impl Display for IchimokuCloud {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(
85 f,
86 "{}({},{},{},{})",
87 self.name(),
88 self.tenkan_period,
89 self.kijun_period,
90 self.senkou_period,
91 self.displacement,
92 )
93 }
94}
95
96impl Indicator for IchimokuCloud {
97 fn name(&self) -> String {
98 stringify!(IchimokuCloud).to_string()
99 }
100
101 fn has_inputs(&self) -> bool {
102 self.has_inputs
103 }
104
105 fn initialized(&self) -> bool {
106 self.initialized
107 }
108
109 fn handle_bar(&mut self, bar: &Bar) {
110 self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
111 }
112
113 fn reset(&mut self) {
114 self.highs.clear();
115 self.lows.clear();
116 self.senkou_a_history.clear();
117 self.senkou_b_history.clear();
118 self.close_history.clear();
119 self.tenkan_sen = 0.0;
120 self.kijun_sen = 0.0;
121 self.senkou_span_a = 0.0;
122 self.senkou_span_b = 0.0;
123 self.chikou_span = 0.0;
124 self.has_tenkan = false;
125 self.has_kijun = false;
126 self.has_senkou_a = false;
127 self.has_senkou_b = false;
128 self.has_chikou = false;
129 self.count = 0;
130 self.has_inputs = false;
131 self.initialized = false;
132 }
133}
134
135impl IchimokuCloud {
136 #[must_use]
146 pub fn new(
147 tenkan_period: usize,
148 kijun_period: usize,
149 senkou_period: usize,
150 displacement: usize,
151 ) -> Self {
152 Self::new_checked(tenkan_period, kijun_period, senkou_period, displacement).expect(FAILED)
153 }
154
155 pub(crate) fn new_checked(
156 tenkan_period: usize,
157 kijun_period: usize,
158 senkou_period: usize,
159 displacement: usize,
160 ) -> anyhow::Result<Self> {
161 anyhow::ensure!(
162 tenkan_period <= MAX_PERIOD,
163 "tenkan_period must not exceed {MAX_PERIOD}"
164 );
165 anyhow::ensure!(
166 kijun_period <= MAX_PERIOD,
167 "kijun_period must not exceed {MAX_PERIOD}"
168 );
169 anyhow::ensure!(
170 senkou_period <= MAX_PERIOD,
171 "senkou_period must not exceed {MAX_PERIOD}"
172 );
173 anyhow::ensure!(
174 displacement <= MAX_PERIOD,
175 "displacement must not exceed {MAX_PERIOD}"
176 );
177
178 anyhow::ensure!(
179 tenkan_period > 0,
180 "IchimokuCloud: tenkan_period must be > 0 (received {tenkan_period})"
181 );
182 anyhow::ensure!(
183 kijun_period > 0,
184 "IchimokuCloud: kijun_period must be > 0 (received {kijun_period})"
185 );
186 anyhow::ensure!(
187 senkou_period > 0,
188 "IchimokuCloud: senkou_period must be > 0 (received {senkou_period})"
189 );
190 anyhow::ensure!(
191 displacement > 0,
192 "IchimokuCloud: displacement must be > 0 (received {displacement})"
193 );
194 anyhow::ensure!(
195 kijun_period >= tenkan_period,
196 "IchimokuCloud: kijun_period must be >= tenkan_period"
197 );
198 anyhow::ensure!(
199 senkou_period >= kijun_period,
200 "IchimokuCloud: senkou_period must be >= kijun_period"
201 );
202 Ok(Self {
203 tenkan_period,
204 kijun_period,
205 senkou_period,
206 displacement,
207 tenkan_sen: 0.0,
208 kijun_sen: 0.0,
209 senkou_span_a: 0.0,
210 senkou_span_b: 0.0,
211 chikou_span: 0.0,
212 has_tenkan: false,
213 has_kijun: false,
214 has_senkou_a: false,
215 has_senkou_b: false,
216 has_chikou: false,
217 count: 0,
218 initialized: false,
219 has_inputs: false,
220 highs: VecDeque::with_capacity(senkou_period),
221 lows: VecDeque::with_capacity(senkou_period),
222 senkou_a_history: VecDeque::with_capacity(displacement),
223 senkou_b_history: VecDeque::with_capacity(displacement),
224 close_history: VecDeque::with_capacity(displacement),
225 })
226 }
227
228 pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
230 if !is_valid_hlc(high, low, close) {
231 return;
232 }
233
234 self.count += 1;
235 self.has_inputs = true;
236
237 if self.highs.len() == self.senkou_period {
238 self.highs.pop_front();
239 self.lows.pop_front();
240 }
241 self.highs.push_back(high);
242 self.lows.push_back(low);
243
244 if self.highs.len() >= self.tenkan_period {
245 self.tenkan_sen = self.midpoint(self.tenkan_period);
246 self.has_tenkan = true;
247 }
248
249 if self.highs.len() >= self.kijun_period {
250 self.kijun_sen = self.midpoint(self.kijun_period);
251 self.has_kijun = true;
252 }
253 let senkou_b_now = if self.highs.len() >= self.senkou_period {
254 self.midpoint(self.senkou_period)
255 } else {
256 f64::NAN
257 };
258 let senkou_a_now = if self.has_tenkan && self.has_kijun {
259 f64::midpoint(self.tenkan_sen, self.kijun_sen)
260 } else {
261 f64::NAN
262 };
263
264 Self::push_capped(&mut self.senkou_a_history, senkou_a_now, self.displacement);
267 Self::push_capped(&mut self.senkou_b_history, senkou_b_now, self.displacement);
268 Self::push_capped(&mut self.close_history, close, self.displacement);
269
270 if let Some(value) = Self::displaced(&self.senkou_a_history, self.displacement) {
271 self.senkou_span_a = value;
272 self.has_senkou_a = true;
273 }
274
275 if let Some(value) = Self::displaced(&self.senkou_b_history, self.displacement) {
276 self.senkou_span_b = value;
277 self.has_senkou_b = true;
278 }
279
280 if let Some(value) = Self::displaced(&self.close_history, self.displacement) {
281 self.chikou_span = value;
282 self.has_chikou = true;
283 }
284
285 self.initialized = self.has_tenkan
286 && self.has_kijun
287 && self.has_senkou_a
288 && self.has_senkou_b
289 && self.has_chikou;
290 }
291
292 fn midpoint(&self, n: usize) -> f64 {
294 let len = self.highs.len();
295 let start = len - n;
296 let mut hi = f64::NEG_INFINITY;
297 let mut lo = f64::INFINITY;
298
299 for i in start..len {
300 hi = hi.max(self.highs[i]);
301 lo = lo.min(self.lows[i]);
302 }
303 f64::midpoint(hi, lo)
304 }
305
306 fn push_capped(queue: &mut VecDeque<f64>, value: f64, cap: usize) {
307 if queue.len() == cap {
308 queue.pop_front();
309 }
310 queue.push_back(value);
311 }
312
313 fn displaced(queue: &VecDeque<f64>, cap: usize) -> Option<f64> {
315 if queue.len() == cap && !queue[0].is_nan() {
316 Some(queue[0])
317 } else {
318 None
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use rstest::rstest;
326
327 use super::*;
328 use crate::indicator::Indicator;
329
330 fn classic() -> IchimokuCloud {
331 IchimokuCloud::new(9, 26, 52, 26)
332 }
333
334 fn ramp(n: i32) -> Vec<(f64, f64, f64)> {
335 (0..n)
336 .map(|i| {
337 let p = 100.0 + f64::from(i);
338 (p + 2.0, p - 2.0, p + 1.0)
339 })
340 .collect()
341 }
342
343 #[rstest]
344 fn test_name_and_display() {
345 let ichi = classic();
346 assert_eq!(ichi.name(), "IchimokuCloud");
347 assert_eq!(format!("{ichi}"), "IchimokuCloud(9,26,52,26)");
348 assert_eq!(ichi.tenkan_period, 9);
349 assert_eq!(ichi.kijun_period, 26);
350 assert_eq!(ichi.senkou_period, 52);
351 assert_eq!(ichi.displacement, 26);
352 assert!(!ichi.initialized());
353 assert!(!ichi.has_inputs());
354 }
355
356 #[rstest]
357 #[should_panic(expected = "tenkan_period must be > 0")]
358 fn test_zero_tenkan_period_panics() {
359 let _ = IchimokuCloud::new(0, 26, 52, 26);
360 }
361
362 #[rstest]
363 #[should_panic(expected = "kijun_period must be > 0")]
364 fn test_zero_kijun_period_panics() {
365 let _ = IchimokuCloud::new(9, 0, 52, 26);
366 }
367
368 #[rstest]
369 #[should_panic(expected = "senkou_period must be > 0")]
370 fn test_zero_senkou_period_panics() {
371 let _ = IchimokuCloud::new(9, 26, 0, 26);
372 }
373
374 #[rstest]
375 #[should_panic(expected = "displacement must be > 0")]
376 fn test_zero_displacement_panics() {
377 let _ = IchimokuCloud::new(9, 26, 52, 0);
378 }
379
380 #[rstest]
381 #[should_panic(expected = "kijun_period must be >= tenkan_period")]
382 fn test_kijun_below_tenkan_panics() {
383 let _ = IchimokuCloud::new(27, 26, 52, 26);
384 }
385
386 #[rstest]
387 #[should_panic(expected = "senkou_period must be >= kijun_period")]
388 fn test_senkou_below_kijun_panics() {
389 let _ = IchimokuCloud::new(9, 26, 25, 26);
390 }
391
392 #[rstest]
393 fn test_warmup_boundaries_per_line() {
394 let mut ichi = classic();
395 for (i, &(h, l, c)) in ramp(80).iter().enumerate() {
396 ichi.update_raw(h, l, c);
397 let bars = i + 1;
398 assert_eq!(ichi.has_tenkan, bars >= 9, "tenkan at bar {bars}");
399 assert_eq!(ichi.has_kijun, bars >= 26, "kijun at bar {bars}");
400 assert_eq!(ichi.has_chikou, bars >= 26, "chikou at bar {bars}");
401 assert_eq!(ichi.has_senkou_a, bars >= 51, "senkou_a at bar {bars}");
403 assert_eq!(ichi.has_senkou_b, bars >= 77, "senkou_b at bar {bars}");
405 assert_eq!(ichi.initialized(), bars >= 77, "initialized at bar {bars}");
406 }
407 }
408
409 #[rstest]
410 fn test_ramp_tenkan_equals_window_midpoint() {
411 let mut ichi = classic();
414 for &(h, l, c) in &ramp(9) {
415 ichi.update_raw(h, l, c);
416 }
417 assert!(ichi.has_tenkan);
418 assert_eq!(ichi.tenkan_sen, 104.0);
419 }
420
421 #[rstest]
422 fn test_chikou_is_close_displacement_bars_back() {
423 let candles = ramp(60);
424 let mut ichi = classic();
425 for (i, &(h, l, c)) in candles.iter().enumerate() {
426 ichi.update_raw(h, l, c);
427
428 if i + 1 >= 26 {
429 assert_eq!(ichi.chikou_span, candles[i + 1 - 26].2, "bar {}", i + 1);
431 }
432 }
433 }
434
435 #[rstest]
436 fn test_senkou_a_is_midpoint_of_lines_displacement_bars_back() {
437 let candles = ramp(80);
438 let mut reference = classic();
439 let mut history = Vec::new();
440
441 for &(h, l, c) in &candles {
442 reference.update_raw(h, l, c);
443 history.push(if reference.has_tenkan && reference.has_kijun {
444 Some(f64::midpoint(reference.tenkan_sen, reference.kijun_sen))
445 } else {
446 None
447 });
448 }
449 let mut ichi = classic();
450 for (i, &(h, l, c)) in candles.iter().enumerate() {
451 ichi.update_raw(h, l, c);
452
453 if i + 1 >= 26
454 && let Some(want) = history[i + 1 - 26]
455 {
456 assert_eq!(ichi.senkou_span_a, want, "bar {}", i + 1);
457 }
458 }
459 }
460
461 #[rstest]
462 fn test_reset() {
463 let mut ichi = classic();
464 for &(h, l, c) in &ramp(100) {
465 ichi.update_raw(h, l, c);
466 }
467 assert!(ichi.initialized());
468 ichi.reset();
469 assert!(!ichi.initialized());
470 assert!(!ichi.has_inputs());
471 assert_eq!(ichi.tenkan_sen, 0.0);
472 assert_eq!(ichi.kijun_sen, 0.0);
473 assert_eq!(ichi.senkou_span_a, 0.0);
474 assert_eq!(ichi.senkou_span_b, 0.0);
475 assert_eq!(ichi.chikou_span, 0.0);
476 assert!(!ichi.has_tenkan);
477 assert!(!ichi.has_kijun);
478 assert!(!ichi.has_senkou_a);
479 assert!(!ichi.has_senkou_b);
480 assert!(!ichi.has_chikou);
481 assert_eq!(ichi.count, 0);
482
483 let mut fresh = classic();
485
486 for &(h, l, c) in &ramp(90) {
487 ichi.update_raw(h, l, c);
488 fresh.update_raw(h, l, c);
489 }
490 assert_eq!(ichi.tenkan_sen, fresh.tenkan_sen);
491 assert_eq!(ichi.kijun_sen, fresh.kijun_sen);
492 assert_eq!(ichi.senkou_span_a, fresh.senkou_span_a);
493 assert_eq!(ichi.senkou_span_b, fresh.senkou_span_b);
494 assert_eq!(ichi.chikou_span, fresh.chikou_span);
495 }
496
497 #[rstest]
498 fn test_custom_periods_accepted() {
499 let mut ichi = IchimokuCloud::new(5, 10, 20, 10);
500 for &(h, l, c) in &ramp(40) {
501 ichi.update_raw(h, l, c);
502 }
503 assert!(ichi.initialized());
504 }
505}