1use super::over_finite_bars;
29use crate::model::Bar;
30use crate::studies::palette;
31use crate::studies::stepper::{rolling_max, rolling_min};
32use crate::studies::stepper::{update_window, usable_behind};
33use crate::studies::{Indicator, IndicatorValue, IndicatorValues};
34use egui::Color32;
35
36#[derive(Clone)]
44pub struct IchimokuCloud {
45 tenkan_period: usize,
46 kijun_period: usize,
47 senkou_b_period: usize,
48 displacement: usize,
49 values: IndicatorValues,
50 tenkan_color: Color32,
51 kijun_color: Color32,
52 span_a_color: Color32,
53 span_b_color: Color32,
54 chikou_color: Color32,
55 cloud_colors: [Color32; 2],
56 visible: bool,
57}
58
59impl IchimokuCloud {
60 pub fn new(tenkan_period: usize, kijun_period: usize, senkou_b_period: usize) -> Self {
67 let kijun_period = kijun_period.max(1);
68 Self {
69 tenkan_period: tenkan_period.max(1),
70 kijun_period,
71 senkou_b_period: senkou_b_period.max(1),
72 displacement: kijun_period,
73 values: IndicatorValues::new(),
74 tenkan_color: palette::BLUE,
75 kijun_color: palette::DARK_RED,
76 span_a_color: palette::LIGHT_GREEN,
77 span_b_color: Color32::from_rgb(239, 154, 154),
78 chikou_color: palette::DARK_GREEN,
79 cloud_colors: [
80 palette::DARK_GREEN.gamma_multiply_u8(26),
81 Color32::from_rgba_unmultiplied(244, 67, 54, 26),
82 ],
83 visible: true,
84 }
85 }
86
87 pub fn default_params() -> Self {
89 Self::new(9, 26, 52)
90 }
91
92 pub fn with_displacement(mut self, displacement: usize) -> Self {
95 self.displacement = displacement;
96 self
97 }
98}
99
100impl Default for IchimokuCloud {
102 fn default() -> Self {
103 Self::new(9, 26, 52)
104 }
105}
106
107impl IchimokuCloud {
108 fn compute(&self, data: &[Bar]) -> IndicatorValues {
110 let mut values = IndicatorValues::new();
111 let mid = |period: usize| -> Vec<Option<f64>> {
113 let highest = rolling_max(data.iter().map(|b| b.high), period);
114 let lowest = rolling_min(data.iter().map(|b| b.low), period);
115 highest
116 .into_iter()
117 .zip(lowest)
118 .enumerate()
119 .map(|(i, (hh, ll))| (i + 1 >= period).then_some((hh + ll) / 2.0))
120 .collect()
121 };
122 let tenkan_line = mid(self.tenkan_period);
123 let kijun_line = mid(self.kijun_period);
124 let senkou_b_line = mid(self.senkou_b_period);
125 for i in 0..data.len() {
126 let tenkan = tenkan_line[i];
127 let kijun = kijun_line[i];
128 let span_a = tenkan
129 .zip(kijun)
130 .map(|(tenkan, kijun)| (tenkan + kijun) / 2.0);
131 let span_b = senkou_b_line[i];
132 let chikou = Some(data[i].close);
133
134 let row = [tenkan, kijun, span_a, span_b, chikou];
135 values.push(if row.iter().all(Option::is_none) {
136 IndicatorValue::None
137 } else {
138 IndicatorValue::Multiple(row.map(|v| v.unwrap_or(f64::NAN)).to_vec())
139 });
140 }
141
142 values
143 }
144}
145
146impl Indicator for IchimokuCloud {
147 crate::studies::inputs::study_inputs!(self;
148 tenkan_period: length("Tenkan period", 1, usize::MAX),
149 kijun_period: length("Kijun period", 1, usize::MAX),
150 senkou_b_period: length("Senkou b period", 1, usize::MAX),
151 displacement: length("Displacement", 0, usize::MAX)
152 ; {});
153
154 fn plot_offset(&self, line: usize) -> isize {
155 let shift = self.displacement.saturating_sub(1).min(isize::MAX as usize) as isize;
156 match line {
157 2 | 3 => shift,
158 4 => -shift,
159 _ => 0,
160 }
161 }
162
163 fn fills(&self) -> Vec<crate::studies::StudyFill> {
164 vec![crate::studies::StudyFill {
165 first: crate::studies::FillBoundary::Plot(2),
166 second: crate::studies::FillBoundary::Plot(3),
167 color: self.cloud_colors[0],
168 inverted_color: Some(self.cloud_colors[1]),
169 }]
170 }
171
172 fn name(&self) -> &str {
173 "Ichimoku"
174 }
175
176 fn desc(&self) -> &str {
177 "Ichimoku Cloud - Comprehensive trend indicator"
178 }
179
180 fn calculate(&mut self, data: &[Bar]) {
181 self.update(data, 0);
182 }
183
184 fn update(&mut self, data: &[Bar], from: usize) {
185 let lookback = self
186 .tenkan_period
187 .max(self.kijun_period)
188 .max(self.senkou_b_period)
189 - 1;
190 let mut values = std::mem::take(&mut self.values);
191 update_window(
192 &mut values,
193 data,
194 from,
195 usable_behind(lookback, |b| {
196 b.high.is_finite() && b.low.is_finite() && b.close.is_finite()
197 }),
198 |data| {
199 over_finite_bars(
200 data,
201 |b| b.high.is_finite() && b.low.is_finite() && b.close.is_finite(),
202 |data| self.compute(data),
203 )
204 },
205 );
206 self.values = values;
207 }
208
209 fn is_incremental(&self) -> bool {
210 true
211 }
212
213 fn values(&self) -> &IndicatorValues {
214 &self.values
215 }
216
217 fn colors(&self) -> Vec<Color32> {
218 vec![
219 self.tenkan_color,
220 self.kijun_color,
221 self.span_a_color,
222 self.span_b_color,
223 self.chikou_color,
224 self.cloud_colors[0],
225 self.cloud_colors[1],
226 ]
227 }
228
229 fn set_colors(&mut self, colors: Vec<Color32>) {
230 if !colors.is_empty() {
231 self.tenkan_color = colors[0];
232 }
233 if colors.len() > 1 {
234 self.kijun_color = colors[1];
235 }
236 if colors.len() > 2 {
237 self.span_a_color = colors[2];
238 }
239 if colors.len() > 3 {
240 self.span_b_color = colors[3];
241 }
242 if colors.len() > 4 {
243 self.chikou_color = colors[4];
244 }
245 if colors.len() > 2 {
246 self.cloud_colors[0] = colors.get(5).copied().unwrap_or(self.span_a_color);
247 }
248 if colors.len() > 3 {
249 self.cloud_colors[1] = colors.get(6).copied().unwrap_or(self.span_b_color);
250 }
251 }
252
253 fn is_overlay(&self) -> bool {
254 true }
256
257 fn line_count(&self) -> usize {
258 5 }
260
261 fn is_visible(&self) -> bool {
262 self.visible
263 }
264
265 fn set_visible(&mut self, visible: bool) {
266 self.visible = visible;
267 }
268
269 fn clone_box(&self) -> Box<dyn Indicator> {
270 Box::new(self.clone())
271 }
272
273 fn line_names(&self) -> Vec<String> {
274 vec![
275 format!("Tenkan({})", self.tenkan_period),
276 format!("Kijun({})", self.kijun_period),
277 "Senkou A".to_string(),
278 format!("Senkou B({})", self.senkou_b_period),
279 "Chikou".to_string(),
280 ]
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use chrono::{Duration, Utc};
288
289 #[test]
291 fn spans_are_displaced() {
292 let highs = [
293 12., 14., 13., 15., 17., 16., 18., 20., 19., 21., 23., 22., 20., 19., 21., 24., 25.,
294 23., 22., 26.,
295 ];
296 let lows = [
297 10., 11., 11., 12., 14., 14., 15., 17., 17., 18., 20., 19., 18., 17., 18., 21., 22.,
298 21., 20., 23.,
299 ];
300 let closes = [
301 11., 13., 12., 14., 16., 15., 17., 19., 18., 20., 22., 20., 19., 18., 20., 23., 24.,
302 22., 21., 25.,
303 ];
304 let start = Utc::now();
305 let bars: Vec<Bar> = (0..20)
306 .map(|i| Bar {
307 time: start + Duration::minutes(i as i64),
308 open: closes[i],
309 high: highs[i],
310 low: lows[i],
311 close: closes[i],
312 volume: 1.0,
313 colors: None,
314 payload: None,
315 })
316 .collect();
317 let n = f64::NAN;
318 let expected = [
320 [n, n, n, n, 11.0],
321 [n, n, n, n, 13.0],
322 [12.0, n, n, n, 12.0],
323 [13.0, n, n, n, 14.0],
324 [14.0, 13.5, 13.75, n, 16.0],
325 [14.5, 14.0, 14.25, n, 15.0],
326 [16.0, 14.5, 15.25, n, 17.0],
327 [17.0, 16.0, 16.5, 15.0, 19.0],
328 [17.5, 17.0, 17.25, 15.5, 18.0],
329 [19.0, 17.5, 18.25, 16.0, 20.0],
330 [20.0, 19.0, 19.5, 17.5, 22.0],
331 [20.5, 20.0, 20.25, 18.5, 20.0],
332 [20.5, 20.0, 20.25, 18.5, 19.0],
333 [19.5, 20.0, 19.75, 19.0, 18.0],
334 [19.0, 20.0, 19.5, 20.0, 20.0],
335 [20.5, 20.5, 20.5, 20.5, 23.0],
336 [21.5, 21.0, 21.25, 21.0, 24.0],
337 [23.0, 21.0, 22.0, 21.0, 22.0],
338 [22.5, 21.5, 22.0, 21.0, 21.0],
339 [23.0, 23.0, 23.0, 21.5, 25.0],
340 ];
341
342 let mut ichimoku = IchimokuCloud::new(3, 5, 8);
343 ichimoku.calculate(&bars);
344
345 assert_eq!(ichimoku.values().len(), bars.len());
346 assert_eq!(ichimoku.plot_offset(2), 4);
347 assert_eq!(ichimoku.plot_offset(4), -4);
348 assert_eq!(ichimoku.plot_value(2, 8), Some(13.75));
349 assert_eq!(ichimoku.plot_value(2, 23), Some(23.0));
350 assert_eq!(ichimoku.plot_value(4, 0), Some(16.0));
351 for (periods, offset) in [(0, 0), (1, 0), (26, 25), (usize::MAX, isize::MAX)] {
352 let study = IchimokuCloud::default().with_displacement(periods);
353 assert_eq!(study.plot_offset(2), offset);
354 assert_eq!(study.plot_offset(4), -offset);
355 }
356 for (i, (value, row)) in ichimoku.values().to_rows().iter().zip(expected).enumerate() {
357 let IndicatorValue::Multiple(got) = value else {
358 panic!("bar {i}: {value:?}");
359 };
360 let same = got.len() == 5
361 && got
362 .iter()
363 .zip(row)
364 .all(|(g, e)| (g.is_nan() && e.is_nan()) || *g == e);
365 assert!(same, "bar {i}: {got:?} != {row:?}");
366 }
367 }
368}